... 작성예정
우선 회원가입 기능을 구현하기 위해 수행해야할 유닛 테스트는 다음과 같다.
[] 회원가입 폼 유효성 검사
[] 회원 유형 확인
[] 회원 중복 검사
[] 비밀번호 암호화
[] 회원 데이터 생성
여기서 폼 유효성 검사는 Nestjs의 api request 파이프라인 중 class-validator에 의해 수행할 것이므로 현재 유닛 테스트 단계에서는 확인할 방법이 없다. 따라서 이는 가장 마지막에 e2e 테스트를 수행하면서 검사하기로 하며, 유효성 검사를 제외한 유닛 테스트를 먼저 describe를 통해 템플릿을 만들어두기로 한다.
// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('User Signup', () => {
describe('Check UserType', () => {});
describe('Check user duplication', () => {});
describe('Encrypt user password', () => {});
describe('Save user to database', () => {});
});
});
회원 유형은 enum 형태의 데이터 타입이며 쿼리스트링의 형태로 url로 주어지게 된다. 이때, 회원 유형은 다음의 3가지 경우 중 하나의 값을 가진다.
1. QS가 주어지지 않아 undefined
2. 사전에 정의된 UserType가 아닌 다른 값
3. 사전에 정의된 UserType 값
따라서 3가지 테스트 케이스가 있으며, 이 역시 먼저 템플릿을 만들어두자.
// users/users.service.spec.ts
describe('UsersService', () => {
...생략...
describe('User Signup', () => {
describe('Check UserType', () => {
it.todo('should return false if UserType is undefined.');
it.todo('should return false if UserType is not a valid UserType.');
it.todo('should return true if UserType is a valid UserType.');
});
});
});
먼저 유닛 테스트의 대상이 되는 최소한의 서비스 코드를 구현한다.
// users/users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
checkUserType(userType) {
return true;
}
}
그리고 checkUserType에서 실패하는 테스트 코드를 작성한다.
it('should return false if UserType is undefined.', () => {
const userType = undefined;
const result = service.checkUserType(userType);
expect(result).toBe(false);
});
테스트를 해본다.
● UsersService › User Signup › Check UserType › should return false if UserType is undefined.
expect(received).toBe(expected) // Object.is equality
Expected: false
Received: undefined
아직 checkUserType에 인자로 주어진 값에 대해 어떠한 로직도 수행하지 않기 때문에 undefined를 걸러내는 첫번째 테스트는 실패한다.
이제 첫번째 테스트 케이스를 통과하는 최소한의 서비스 코드를 구현하고 다시 테스트를 돌린다.
checkUserType(userType: undefined): boolean {
return userType !== undefined;
}
✓ should return false if UserType is undefined. (1 ms)
userType가 undefined인지를 확인하는 첫 테스트는 통과했다.
이제 두번째 테스트 케이스를 작성하고 테스트를 돌려보자.
it('should return false if UserType is not a valid UserType.', () => {
const userType = 'admin';
const result = service.checkUserType(userType);
expect(result).toBe(false);
});
● UsersService › User Signup › Check UserType › should return false if UserType is not a valid UserType.
expect(received).toBe(expected) // Object.is equality
Expected: false
Received: true
checkUserType의 인수가 string값이 주어졌고, 그 값이 무엇인지와 관계없이 undefined는 아니기 때문에 의도와는 다르게 true가 반환되었다.
두번째 테스트 케이스를 통과하는 최소한의 서비스코드를 작성하자.
checkUserType(userType: string|undefined): boolean {
return (
userType !== undefined
&& ['customer', 'business'].includes(userType)
);
}
✓ should return false if UserType is not a valid UserType. (2 ms)
두번째 테스트도 통과했다.
하지만 지금 작성된 서비스 코드는 썩 좋아보이지가 않으므로 한번 리팩토링의 과정이 필요해보인다. UserType을 정의하고 이를 활용하여 코드를 한번 정리한다.
const UserType = {
CUSTOMER: 'customer',
BUSINESS: 'business',
} as const;
type UserType = typeof UserType[keyof typeof UserType];
isUserType(userType: string|undefined): userType is UserType {
return Object.values(UserType).includes(userType as UserType);
}
enum 형태의 객체를 만들고, 어차피 이 유닛의 목적이 UserType인지 판별하는 것이라면 타입가드로 메소드를 다시 작성하는 것이 나을 것이다.
앞선 테스트들이 무사히 통과하는지 다시 확인한다.
✓ should return false if UserType is undefined. (2 ms)
✓ should return false if UserType is not a valid UserType. (2 ms)
이제 마지막 테스트 케이스를 작성하고 테스트를 돌려보자.
it('should return true if UserType is a valid UserType.', () => {
const userType = 'customer'
const result = service.isUserType(userType);
expect(result).toBe(true);
});
✓ should return false if UserType is undefined. (2 ms)
✓ should return false if UserType is not a valid UserType. (1 ms)
✓ should return true if UserType is a valid UserType. (2 ms)
문제 없이 모든 테스트를 전부 통과했다!
하지만, 마지막 테스트는 userType을 단 하나의 케이스만 검사하기 때문에 다른 유효한 값이 들어갔을 경우, 혹은 추후 유효한 UserType가 추가 되었을 경우 어떻게 동작할지 알 수가 없다.
이를 보완하여 테스트 코드를 다시 작성해본다.
// types/users.ts
export const UserType = {
CUSTOMER: 'customer',
BUSINESS: 'business',
} as const;
declare global {
type UserType = typeof UserType[keyof typeof UserType];
}
// users/users.service.spec.ts
import { UserType } from 'src/types';
it('should return true if UserType is a valid UserType.', () => {
const userTypes = Object.values(UserType);
const userType = userTypes[Math.floor(Math.random() * userTypes.length)];
const result = service.isUserType(userType);
expect(result).toBe(true);
});
✓ should return false if UserType is undefined. (2 ms)
✓ should return false if UserType is not a valid UserType. (1 ms)
✓ should return true if UserType is a valid UserType. (1 ms)
UserService에서 정의한 UserType을 좀더 보편적으로 사용하기 위해 별도의 모듈로 빼고, UserType은 글로벌로 정의를 하였다.
그리고 테스트에서 이 객체를 가지고 와서, 객체의 랜덤한 값을 테스트에 넣도록 하여 추후 UserType에 새로운 값이 추가된다고해도 이 테스트는 여전히 유효한 테스트가 될 것이다.
** 만약 "Cannot find module 'src/types' from 'users/users.service.ts'
와 같은 에러가 발생한다면, 아래 글을 참고하자.
Jest에서 alias path 사용
이렇게 해서 드디어 첫번째 유닛 테스트를 성공적으로 끝마쳤다.
[보류] 회원가입 폼 유효성 검사
[✅] 회원 유형 확인
[] 회원 중복 검사
[] 비밀번호 암호화
[] 회원 데이터 생성
이제 남은 유닛 테스트들은 외부 의존성을 가지기 때문에 외부 의존성 모듈을 mocking하여야 하는데, 다음 장에서는 Jest에서 Mock객체 혹은 Mock함수를 다루는 방법에 대해 알아보겠다.