1.[rootrip-factoring:test] create-local-user , verification-email-code 테스팅 코드 작성
현재 , 개발 프로세스에서 테스팅은 매우 중요하다. 대략적인 장점으론 ,
1. 버그 발견 및 예방
2. 유지 보수 와 리팩토링
3. 자동화 된 테스트
4. 문서화
등이 있는거 같다.
지엽적인 내용이므로 생략하고 , 핵심은 테스팅 코드를 어떻게 , 어떤 측면으로 짜는지가 핵심이다.
다양한 테스팅 코드를 구경하면서 접목시키려고 했는데, 코드들 마다 관점이 다 달라서 꽤나 혼란스러웠다.
깨달은 점은 어떤 의도로 테스팅 코드를 작성하는지에 따라 달라지는 것 같다.
( 코드를 작성하기 전 , 미리 테스팅 코드를 작성하고 본 코드를 작성하는지 )
( 상세 과정은 포함하지 않고 , 결과 만 테스팅 하는지 )
( 안에 들어있는 로직들을 전부 검사하고 결과도 같이 테스팅 하는지 )
=> 나는 , 2번안 과 3번안의 그 사이를 준수하며 작성한 거 같다.
로직을 전부 검사하지 않고 , 실행이 되었는지 검사만 하여 , 동작이 하는 것을 기대했다.
+ 추가로 , kafka 나 , redis 같은 Provider 들도 같이 추가하여 , 테스트 코드를 작성하여 생긴 고민도 같이 적겠다.
핵심은 Command 를 통해 실행되는 로직을
어떻게 테스팅 할 지와 throw 하는 Error 를 어떻게 테스팅 할지 방법에 대해서 고민하였다.
테스팅 모듈 과 공통 변수 선언부터 설명하겠다.
let controller: CreateLocalUserController;
let commandBus: CommandBus;
let commandResult: Result<
string,
EmailAlreadyExistError | NicknameAlreadyExistError
>;
let expectedResult:
| ResponseBase<{ id: string }>
| EmailAlreadyExistError
| NicknameAlreadyExistError;
let createUserProps: CreateLocalUserProps;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CreateLocalUserController],
providers: [
{
provide: CommandBus,
useValue: {
execute: jest.fn(),
},
},
],
}).compile();
controller = module.get<CreateLocalUserController>(
CreateLocalUserController,
);
commandBus = module.get<CommandBus>(CommandBus);
createUserProps = typia.random<CreateLocalUserProps>();
});
it('0. Command must be created', () => {
const command = new CreateLocalUserCommand(createUserProps);
expect(typia.is<CreateLocalUserCommand>(command)).toBeTruthy();
});
it('1. Should create a new user', async () => {
commandResult = Ok('user_id'); // Replace with the expected command result
expectedResult = match(commandResult, {
Ok: (id: string) => new ResponseBase({ id }),
});
jest.spyOn(commandBus, 'execute').mockResolvedValue(commandResult);
const result = await controller.create(createUserProps);
expect(result).toEqual(expectedResult);
});
it('2. Should handle email already exists throw error ', async () => {
commandResult = Err(new EmailAlreadyExistError());
jest.spyOn(commandBus, 'execute').mockResolvedValue(commandResult);
try {
match(commandResult, {
Err: (error: EmailAlreadyExistError) => {
throw error;
},
});
} catch (err: any) {
expectedResult = err;
return;
}
const result = await controller.create(createUserProps);
expect(result).toEqual(expectedResult);
});
위와 동일하나 CommandBus 처럼 실행이 되는 주체를 mocking 할 필요가 없다.
해당 코드는 , CommandHandler 를 통해 , 이미 실행이 되었다고 가정하기에 , 기능 검증만 하면 된다.
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: USER_REPOSITORY,
useValue: {
findByEmail: jest.fn(),
findByNickname: jest.fn(),
},
},
CreateUserCommandHandler,
EventEmitter2,
],
}).compile();
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
userRepository = module.get<UserRepositoryPort>(USER_REPOSITORY);
createUserHandler = module.get<CreateUserCommandHandler>(
CreateUserCommandHandler,
);
command = new CreateLocalUserCommand(typia.random<CreateLocalUserProps>());
});
it('should return Ok with two events emitting', async () => {
jest.spyOn(eventEmitter, 'emitAsync');
const result = await createUserHandler.execute(command);
expect(result.isOk()).toBe(true);
expect(eventEmitter.emitAsync).toHaveBeenCalledTimes(2);
});
여기서 많이 시간을 썼는데 , eventEmitter 가 어떤 DomainEvent 를 확인하는지로 검사를 해보고 싶었는데 ,
toHaveBeenCalledWith(SendVertificationEmailDomainEvent) 이렇게 검사를 해도 ,
Expected: [Function SendVertificationEmailDomainEvent]
Received
1: "SendVertificationEmailDomainEvent", {"aggregatedId": "8f4279de-ae0e-4421-83a9-fb1d0224b9ab", "email": "uzglhdfwy", "id": "86b7dc37-6df8-42b9-9501-c0addd189fb1", "metadata": {"causationId": undefined, "correlationId": undefined, "timestamp": 1697113329935, "userId": undefined}, "nickname": "ocazcfcll", "redirectCode": "6e36c09af35af7b8b77a3a8ef2aea6a3"}
it('should handle email already exists return error ', async () => {
jest
.spyOn(userRepository, 'findByEmail')
.mockResolvedValue({ id: 'user_id' });
const result = await createUserHandler.execute(command);
expect(result.isErr()).toBe(true);
expect(result.unwrapErr()).toBeInstanceOf(EmailAlreadyExistError);
});
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ProviderModule],
providers: [
{
provide: 'PRODUCER',
useValue: {
send: jest.fn(),
},
},
SendVertificationEmailEventListener,
],
}).compile();
producer = module.get<Producer>(PRODUCER);
await producer.connect();
listener = module.get<SendVertificationEmailEventListener>(
SendVertificationEmailEventListener,
);
event = new SendVertificationEmailDomainEvent(
typia.random<DomainEventProps<SendVertificationEmailDomainEvent>>(),
);
});
it('should send a verification email', async () => {
const expectedMessage = {
email: event.email,
code: event.redirectCode,
nickname: event.nickname,
};
const expectedResult: RecordMetadata[] = [
{
errorCode: 10,
partition: 1,
topicName: SEND_VERTIFICATION_EMAIL,
},
];
jest.spyOn(producer, 'send')
//.mockResolvedValue(expectedResult);
listener.handleSendVertificationEmailEvent(event);
expect(producer.send).toHaveBeenCalledWith({
messages: [
{
value: JSON.stringify(expectedMessage),
},
],
topic: SEND_VERTIFICATION_EMAIL,
acks: 1,
});
});
처음 테스팅 코드를 작성해봤는데 , 단순 구현을 위한 코드와 완벽한 테스팅 호환 가능한 코드 작성은 매우 다른것을 느꼈다.
1. 특히나 , 각 Event 들이나 , Entity 등에는 현재 실행중인 current execution context 에서 받는 request 같은것들이 포함되는데 ,
테스팅때는 포함이 될리가 없으므로 전부 에러가 발생했다.
import { RequestContext } from 'nestjs-request-context';
export class RequestContextService {
static getContext(): AppRequestContext {
const ctx: AppRequestContext = RequestContext.currentContext?.req;
return ctx;
}
}
https://github.com/youngsu5582/RooTrip-Factoring/tree/develop/src/modules/user/test