nest.js에서도 테스트 주도 개발을 해보자.
node.js에서 jest로 테스트 주도 개발을 해보았다.
하지만 node.js에서 할때는 테스트 코드 작성을 어떻게 할지 몰라 api test만 진행했었다.
이번엔 service, controller를 각각 unit test 한다음, 최종적으로 api들을 E2E test할 예정이다.
npm i -D @nestjs/testing
@nestjs/testing
패키지는 nest.js 테스트 코드를 작성할 때 유용한 다양한 유틸리티를 포함하고 있다.
nest.js에서 test를 할때 편리한 점은, nest가 의존성 주입 기반으로 작동하기 때문에 mocking해주기가 쉽다는 것이다.
@Injectable()
export class AuthService {
private slackClient: SlackApiClient;
constructor(
private jwtService: JwtService,
private usersService: UsersService,
) {
this.slackClient = new SlackApiClient();
}
}
예를 들어 이건 AuthService
의 의존성 주입 부분인데, 생성자에서 jwtService와 usersService를 의존성 주입해주고 서비스 함수들에서 jwtService와 usersService를 사용할 수 있게 된다.
이 의존성 주입 부분에 다른 class를 주입해도 정상적으로 돌아간다는 것이다.
class MockUserService {
// userService mocking하기
}
class MockJwtService {
// jwtService mocking하기
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: JwtService,
useClass: MockJwtService,
},
{
provide: UsersService,
useClass: MockUserService,
},
],
}).compile();
authService = module.get<AuthService>(AuthService);
});
따라서 test 부분에서 실제 UsersService
를 사용하는 대신, UsersService
를 mocking하여 만든 class를 대신 주입해주자.
async signIn(data: SigninRequestDto): Promise<Tokens> {
const { snsId } = data;
const user = await this.usersService.findUser('snsId', snsId);
if (!user) {
throw new BadRequestException({
message: '회원가입 되지 않은 유저입니다!',
code: customErrorCode.USER_NOT_AUTHENTICATED,
});
}
.
.
.
}
이렇게 AuthService
의 회원가입코드에는 유저를 조회하는 UserService
의 findUser함수가 사용되는데, 이 함수는 userService에서 테스트해주면 되기 때문이다. AuthService
에는 AuthService
의 함수들만 철저하게 테스트하고 그 하위 함수들은 해당 서비스들에서 철저하게 테스트 해주면 된다.
createAccessToken(userId: number): string {
return this.jwtService.sign(
{ userId: `${userId}` },
{
expiresIn: `${process.env.ACCESS_TOKEN_EXPIRE}`,
},
);
}
그리고 이런 토큰 만드는 함수를 테스트할때, 예상값을 지정해 주어야하는데 토큰은 만들어지는 값을 예상할 수 없기 때문에 mocking 해주어야 한다. 그럼 토큰이 잘 만들어지는지 sign
함수는 어떻게 테스트하나요? 라고 한다면 그건 다시 jwtService
에서 테스트를 하면 된다.
class MockJwtService {
sign() {
return 'test_token';
}
}
따라서 jwtService
를 mocking하여 무조건 'test_token' 라는 값이 return되게 바꾸어 준다면 예상값을 제대로 예측할 수 있고
describe('createRefreshToken', () => {
it('createRefreshToken success', async () => {
const request = 1;
const response = 'test_token';
expect(authService.createRefreshToken(request)).toBe(response);
});
});
이렇게 예상값을 확신할 수 있어 테스트가 용이해진다.
it('test', async () => {
const request = 1;
const response = 'test response';
expect(authService.test(request)).toBe(response);
});
응답 값은 toBe
를 통해 검증한다.
describe('test', () => {
it('test', async () => {
const request = 1;
const response = { test: 'test response' };
expect(authService.test(request)).toStrictEqual(response);
});
});
응답 값이 객체라면 toStrictEqual
를 통해 검사한다.
describe('test', () => {
it('test', async () => {
const request = 1;
expect(authService.test(request)).toBeUndefined();
});
});
return 값이 void인 함수들에 대해서는 toBeUndefined
를 사용하면 된다.
describe('test', () => {
it('test', async () => {
const request = 1;
const response = { test: 'test response' };
expect(authService.test(request)).resolves.toStrictEqual(response);
});
});
만약 비동기 함수에 대해 테스트 할때는 앞에 resolves
를 붙혀주어야 한다.
describe('test', () => {
it('test', async () => {
const request = 1;
const response = new BadRequestException({
message: '이미 가입된 유저입니다!',
code: customErrorCode.USER_ALREADY_EXIST,
});
expect(authService.test(request)).rejects.toThrow(response);
});
});
실패 테스트의 경우 rejects
로 응답을 확인하면 된다.