본문 바로가기
웹 개발/KLUB 프로젝트

Jest 테스트 작성법

by L3m0n S0ju 2023. 7. 27.

 

 

테스트 설계 및 방향

 

Controller -> Service -> Repository

 

현재 KLUB 프로젝트는 설계 방향은 위와 같이 Controller에서 Service를 호출하고 다시 Service에서 Repository를 호출하는 방향으로 설계되어 있습니다.

 

Jest로 테스트를 진행할 부분은 Controller와 Service 파일인데 Controller를 테스트하는 경우 Controller 자체의 기능에 집중하기 위해서 Service를 모킹하고 Service를 테스트하는 경우 Repository를 모킹함으로써 격리된 환경에서 테스트를 진행할 수 있습니다.

 

klub 프로젝트는 프론트에서 e2e 테스트를 진행해주기 때문에 백엔드에서는 통합 테스트를 보다는 단위 테스트를 먼저 진행합니다. Jest로 진행하되 ts-mockito라는 라이브러리를 사용하여 가독성을 높였습니다. when 구문을 통해서 깔끔하게 함수들을 모킹할 수 있고 여러 기능들을 제공합니다. 기능들은 https://github.com/NagRock/ts-mockito 공식 깃허브에서 확인할 수 있다. 

 

테스트 데이터는 아래 파일처럼 test 디렉토리 하위에 가짜 데이터를 만들고 진행했습니다. 실제 테스트 데이터를 작성할 때는 club.service.testData.ts, club.controller.testData.ts 처럼 각서비스와 컨트롤러마다 테스트 데이터 파일을 만들고 해당 파일 안에 사용할 테스트 데이터를 작성하면 됩니다.

 

 

userTestData.ts 일부

// User01
const testUser01: User = new User();
testUser01.id = 1;
testUser01.studentId = '1000000001';
testUser01.nickname = 'testNickName';
testUser01.isAuthenticated = true;
testUser01.password = '1q2w3e4r';
testUser01.refreshToken = 'test';
testUser01.permission = userPermission.USER;
testUser01.email = 'test01@korea.ac.kr';
testUser01.refreshToken = 'test';
testUser01.name = 'testName';
testUser01.phoneNumber = '010-0000-0001';
testUser01.department = '정보대학 컴퓨터학과';

// User02
const testUser02: User = new User();
testUser02.id = 2;
testUser02.studentId = '1000000002';
testUser02.nickname = 'testNickName';
testUser02.isAuthenticated = true;
testUser02.password = '1q2w3e4r';
testUser02.refreshToken = 'test';
testUser02.permission = userPermission.USER;
testUser02.email = 'test02@korea.ac.kr';
testUser02.refreshToken = 'test';
testUser02.name = 'testName';
testUser02.phoneNumber = '010-0000-0001';
testUser02.department = '정보대학 컴퓨터학과';

// {User01} 비밀번호가 다른 경우
const testUser03: User = new User();
testUser03.id = 1;
testUser03.studentId = '1000000001';
testUser03.nickname = 'testNickName';
testUser03.isAuthenticated = true;
testUser03.password = '4r3e2w1q';
testUser03.refreshToken = 'test';
testUser03.permission = userPermission.USER;
testUser03.email = 'test01@korea.ac.kr';
testUser03.name = 'testName';
testUser03.phoneNumber = '010-0000-0001';
testUser03.department = '정보대학 컴퓨터학과';

// {User01} isAuthenticated: false인 경우
const testUser04: User = new User();
testUser04.id = 1;
testUser04.studentId = '1000000001';
testUser04.nickname = 'testNickName';
testUser04.isAuthenticated = false;
testUser04.password = '1q2w3e4r';
testUser04.refreshToken = null;
testUser04.permission = userPermission.USER;
testUser04.email = 'test01@korea.ac.kr';
testUser04.name = 'testName';
testUser04.phoneNumber = '010-0000-0001';
testUser04.department = '정보대학 컴퓨터학과';

// {User01} password: null, isAuthenticated: false인 경우
const testUser05: User = new User();
testUser05.id = 1;
testUser05.studentId = '1000000001';
testUser05.nickname = 'testNickName';
testUser05.isAuthenticated = false;
testUser05.password = null;
testUser05.refreshToken = null;
testUser05.permission = userPermission.USER;
testUser05.email = 'test01@korea.ac.kr';
testUser05.name = 'testName';
testUser05.phoneNumber = '010-0000-0001';
testUser05.department = '정보대학 컴퓨터학과';

 

 

 

 

 


Test파일에서 Mocking하기

 

mocking 파일을 따로 만들어서 함수들을 구현할 생각이었으나 한 개의 모킹 함수로는 여러 테스트의 요구 조건을 맞출 수 없다는 문제점이 있습니다. 같은 함수라도 사용되는 곳에 따라 다양한 값을 반환해야하는 경우 여러개의 모킹 함수를 작성해야 합니다. 따라서 mocking 파일을 만들지 않고 ts-mockito 라이브러리를 사용하서 각 테스트마다 사용할 모킹 함수를 편하게 구현할 수 있습니다.

 

모듈 설정 부분은 따로 뺄까 고민을 해봤지만 ts-mockito를 사용하면 설정 부분이 필요없어서 따로 모듈 파일을 만들지 않아도 된다.

 

테스트 파일 예시는 아래 코드와 같습니다. it을 기준으로 단위 테스트를 진행하면 됩니다. when으로 함수를 모킹할 때 anything()으로 입력 제한을 널널하게 받을까 생각했지만 그래도 최대한 제한을 좁게 주는게 테스트 목적에 맞는 같습니다. 상세한 입력값 제한을 두려면 deepEqual을 사용하면 정확한 비교가 이루어진다. 일반적인 경우는 toBe로 충분하다.

 

 

 

 

club.service.spec.ts 일부

const userRepositoryMock = mock(UserRepository);
const clubMemberRepositoryMock = mock(ClubMemberRepository);
const clubRepositoryMock = mock(ClubRepository);

const userService = new UserService(
  instance(userRepositoryMock),
  instance(clubMemberRepositoryMock),
  instance(clubRepositoryMock),
);

describe('UserService', () => {
it('validateUser - 이메일이 인증되지 않은 user인 경우', () => {
    when(
      userRepositoryMock.getUserCredentialByOrFail(
        deepEqual({ email: testUser04.email }),
      ),
    ).thenResolve(testUserCredential04);
    expect(async () => {
      await userService.validateUser(testUser04.email);
    }).rejects.toThrowError(
      '가입하신 이메일이 인증되지 않았습니다 :( 학교 메일을 확인하시고 인증을 완료해주세요.',
    );
  });

  it('validateUser - 존재하지 않는 user인 경우', () => {
    when(
      userRepositoryMock.getUserCredentialByOrFail(
        deepEqual({ email: testUser05.email }),
      ),
    ).thenResolve(testUserCredential05);
    expect(async () => {
      await userService.validateUser(testUser05.email);
    }).rejects.toThrowError(
      '존재하지 않는 계정입니다. 서비스를 이용하시려면 회원 가입 해주세요.',
    );
  });
  
it('checkExistence - 해당 학번에 해당하는 user가 미인증 상태인 경우', async () => {
    when(
      userRepositoryMock.findOne(
        deepEqual({
          where: { studentId: testUser04.studentId },
          withDeleted: true,
        }),
      ),
    ).thenResolve(testUser04);
    await expect(
      userService.checkExistence(testUser04.studentId, testUser04.email),
    ).resolves.toBe(testUser04.id);
  });

  it('checkExistence - 해당 이메일에 해당하는 user가 미인증 상태인 경우', async () => {
    when(
      userRepositoryMock.findOne(
        deepEqual({
          where: { email: testUser04.email },
          withDeleted: true,
        }),
      ),
    ).thenResolve(testUser04);
    await expect(
      userService.checkExistence(testUser04.studentId, testUser04.email),
    ).resolves.toBe(testUser04.id);
  });

  it('checkExistence - user 데이터가 존재하지 않는 경우', async () => {
    when(userRepositoryMock.findOne(anything())).thenResolve(null);
    await expect(
      userService.checkExistence(testUser04.studentId, testUser04.email),
    ).resolves.toBe(null);
  });

  it('checkExistence - 이미 가입 완료된 학번인 경우', async () => {
    when(
      userRepositoryMock.findOne(
        deepEqual({
          where: { studentId: testUser01.studentId },
          withDeleted: true,
        }),
      ),
    ).thenResolve(testUser01);
    expect(async () => {
      await userService.checkExistence(testUser01.studentId, testUser01.email);
    }).rejects.toThrowError('이미 KLUB에 가입 완료된 학번입니다.');
  });

  it('checkExistence - 이미 가입 완료된 이메일인 경우', async () => {
    when(userRepositoryMock.findOne(anything())).thenReturn(null);
    when(
      userRepositoryMock.findOne(
        deepEqual({
          where: { email: testUser01.email },
          withDeleted: true,
        }),
      ),
    ).thenResolve(testUser01);
    expect(async () => {
      await userService.checkExistence(testUser01.studentId, testUser01.email);
    }).rejects.toThrowError('이미 KLUB에 가입 완료된 이메일입니다.');
  });
});

 

 

 

 


후기

사실 스프링과 junit으로 하면 알아서 인메모리 모드로 DB 모킹해줘서 실제 서버 사용하듯이 편하게 할 수 있는데 jest는 그런 기능이 없고 직접 모킹해줘야 돼서 꽤 불편하다. 알아서 모킹해주는 라이브러리를 찾아봤는데 스프링, junit 같은 라이브러리는 없고 결국은 nestjs에서 기본으로 제공하는 jest에 추가로 ts-mockito 같은 라이브러리를 사용하는게 보편적인 방법인 것 같다. 테스트 코드를 작성하면서 조금 오래 걸렸던 부분은 서비스에서 서비스 함수를 가져오고 또 서비스 함수를 가져오는 것 처럼 너무 많은 메소드를 거치면 테스트를 할 때 모킹해야할 레포지토리 함수가 많아져서 힘들었던 것 같다. 그런 코드를 보면 보통 한 메소드에서 여러 기능을 해결하려고 할 때 발생하는 문제였다. 앞으로는 한 메소드 안에는 한가지 기능을 넣으려고 노력해야겠다는 생각이 들었다.

 

끝까지 읽어주셔서 감사합니다.

'웹 개발 > KLUB 프로젝트' 카테고리의 다른 글

배포 관련 링크 창고  (0) 2023.06.24
klub 1.2.0 버전 배포  (0) 2023.06.04
klub 배포전 QA 진행  (0) 2023.05.28
git flow 전략이란?  (0) 2023.05.26
klub 개발 일지-20230519  (0) 2023.05.19

댓글