본문 바로가기
Back-end/nest.js

Nest.js : Ports and Adapters Architecture 구현하기 (3) - Test

by hsloth 2023. 2. 26.

 

Ports and Adapters Architecture를 구현하면서 테스트 코드를 작성중에 생긴일이다.

또한, Jest를 이용하여 void 함수를 어떻게 처리하냐에 대한 내용도 있다!

 

테스트를 작성하는데, 나는 기능 별로 port를 만들어 port마다 함수를 하나씩 작성한게 아니라

Service 별로(리소스 별로) port를 만들어서 거기다가 함수를 몽땅 때려박는 형식으로 작성을 했다.

그러다 보니 유닛 테스트 코드를 작성할 때, 자그마한 문제가 발생했다.

 

먼저, 내가 처음에 짜둔 코드를 봐보자.

 

// auth.service.spec.ts
class MockUserRepositoryOutboundPort implements UserRepositoryOutboundPort {
  private readonly result;

  constructor(result) {
    this.result = result;
  }

  async getUserForLogIn(
    params: FindUserForLogInOutboundPortInputDto,
  ): Promise<FindUserForLogInOutboundPortOutputDto> {
    return this.result;
  }
  async findUserByEmail(
    params: FindUserByEmailOutboundPortInputDto,
  ): Promise<FindUserByEmailOutboundPortOutputDto> {
    return this.result;
  }
  async saveUser(params: SaveUserOutboundPortInputDto): Promise<unknown> {
    return this.result;
  }
}

...

  test('Validate User Test', async () => {
    const user: FindUserForLogInOutboundPortOutputDto = {
      id: '1',
      createdAt: new Date('2023-01-01'),
      updatedAt: null,
      deletedAt: null,
      email: 'develop@google.com',
      password: await hash('1234', 10),
      googleId: null,
    };

    const authService = new AuthService(
      new MockUserRepositoryOutboundPort(user),
      new MockConfigServiceOutboundPort(null),
    );
    const res = await authService.validateUser({
      email: 'develop@google.com',
      password: '1234',
    });

    expect(res).toStrictEqual({
      id: '1',
      createdAt: new Date('2023-01-01'),
      updatedAt: null,
      deletedAt: null,
      email: 'develop@google.com',
      googleId: null,
    });

이런식으로 result를 생성자의 인자로 받아 함수의 결과로 나오게 설정을 하였다.

그런데, 문제가 발생했다.

다음 auth.service.ts 로직을 봐보자.

// auth.service.ts

  async register(
    params: RegisterInboundInputDto,
  ): Promise<RegisterInboundOutputDto> {
    const hashedPassword = await hash(
      params.password,
      await this.configServiceOutboundPort.getSaltForHash(),
    );
    
    // 이곳에서 UserRepository가 한 번
    const existedUser = await this.userRepositoryOutboundPort.findUserByEmail({
      email: params.email,
    });

    if (existedUser) {
      throw new BadRequestException(ERROR_MESSAGE.FAIL_TO_REGISTER_EMAIL);
    }

	// 이곳에서 UserRepository가 한 번
    const user = await this.userRepositoryOutboundPort.saveUser({
      email: params.email,
      hashedPassword: hashedPassword,
    });

    if (!user) {
      throw new BadRequestException(ERROR_MESSAGE.FAIL_TO_CREATE_USER);
    }
  }

authService에서 UserRepositoryOutboundPort를 두 번이나 연속해서 사용한 것이다.

이렇게 되면 생성자에 인자가 하나밖에 들어갈 수 없으므로 UserRepositoryOutboundPort의 어떤 함수든 값이 동일하게 나오기 때문에 이런 방법은 사용이 불가능하다. (내가 멍청했다........)

 

그.래.서

이 방법이 최선은 아니겠지만, 생성자의 인자의 타입을 만들어서 설정해 주었다.

// auth.service.spec.ts

// 각 함수마다 결과 값을 받을 수 있도록 타입을 정의했다.
type MockUserRepositoryOutboundPortParamType = {
  findUserForLogIn?: FindUserForLogInOutboundPortOutputDto;
  findUserByEmail?: FindUserByEmailOutboundPortOutputDto;
  saveUser?: unknown;
};
class MockUserRepositoryOutboundPort implements UserRepositoryOutboundPort {
  // 타입 추론이 가능하게 타입을 설정해주었다.
  private readonly result: MockUserRepositoryOutboundPortParamType;

  constructor(result: MockUserRepositoryOutboundPortParamType) {
    this.result = result;
  }

  async findUserForLogIn(
    params: FindUserForLogInOutboundPortInputDto,
  ): Promise<FindUserForLogInOutboundPortOutputDto> {
    return this.result.findUserForLogIn;
  }
  async findUserByEmail(
    params: FindUserByEmailOutboundPortInputDto,
  ): Promise<FindUserByEmailOutboundPortOutputDto> {
    return this.result.findUserByEmail;
  }
  async saveUser(params: SaveUserOutboundPortInputDto): Promise<unknown> {
    return this.result.saveUser;
  }
}


...
  test('Register User Test', async () => {

      const existedUser = undefined;  // 회원가입 시, 이미 존재하는 email이 아니어야 가입이 가능하므로 undefined를 할당한다.
      const user = {
        email: 'develop@google.com',
      };

	  // AuthService를 만드는데, Mock을 이용한다. (Port를 사용하기 때문에 실제 구현체에 의존적이지 않다)
      const authService = new AuthService(
        new MockUserRepositoryOutboundPort({  // 해당 테스트에서 findUserByEmail과 saveUser 
          findUserByEmail: existedUser,  // 라는 함수가 필요하니 해당 함수에 대한 결과값을 인자로 넣어준다.
          saveUser: 1,
        }),
        new MockConfigServiceOutboundPort({ getSaltForHash: 1 }),
      );

      const res = await authService.register({
        email: user.email,
        password: '1234',
      });
    }
  });

짠! 다 짰다! 그런데 뭔가 허전하지 않나...? 싶어서 봤는데

authService.register() 함수는 return 값이 void 이다...!

expect를 어떻게 써야 void 함수를 테스트할 수 있지? 라는 생각이 들었다.

 

그래서 주위에 자문을 구해본 결과... try-catch 문을 사용해서 테스트를 짰다.

별거 없다. 그냥 try-catch 문으로 덮어주면 된다.

  test('Register User Test', async () => {
    try {
      const existedUser = undefined;  
      const user = {
        email: 'develop@google.com',
      };

      const authService = new AuthService(
        new MockUserRepositoryOutboundPort({  
          findUserByEmail: existedUser,  
          saveUser: 1,
        }),
        new MockConfigServiceOutboundPort({ getSaltForHash: 1 }),
      );

      const res = await authService.register({
        email: user.email,
        password: '1234',
      });
    } catch (err) {
      // 만약에 에러가 생기면 에러를 뱉도록 한다. 1 != 2 니까 무조건 에러를 뱉을 것이다.
      expect(1).toBe(2);
    }

 

흐.. 테스트 어렵다...