포시코딩

1월29일 - Test Code 작성하면서 궁금한거 정리 본문

TIL

1월29일 - Test Code 작성하면서 궁금한거 정리

포시 2023. 1. 29. 18:53
728x90

개요

테스트 코드를 작성하면서 의문이 들었던 부분이나 헤맸던 부분을 정리해봄

 

return이 avoid type인 메소드

users.repo

createUser = async (userInfo) => {
  await this.usersModel.create({
    email: userInfo.email,
    password: userInfo.password,
    // ...생략
  });
};

unit test code

test('users.repository createUser Method success', async () => {
    mockUsersModel.create = jest.fn(() => {
      return 'test';
    });

    const userInfo = {
      email: 'test@gmail.com',
      password: 'password',
      // ...생략
    };
    await usersRepository.createUser(userInfo);

    expect(mockUsersModel.create).toHaveBeenCalledTimes(1);
    expect(mockUsersModel.create).toHaveBeenCalledWith({
      email: userInfo.email,
      password: userInfo.password,
      // ...생략
    });
  });

createUser 메소드를 실행했을 때의 결과값이 존재하지 않아

해당 메소드의 결과를 비교할 수단이 없다. 

 

다른 리턴값이 있는 메소드의 경우 

const result = await usersRepository.findOneByEmail('test@gmail.com');

expect(result).toEqual('test');

이런식으로 비교 테스트를 넣을 수 있지만

위 createUser는 불가능한 상태.. 이럴 때 다른 방법이 있을지 내가 한 방법이 맞는지 궁금

 

sequelize transaction 사용하는 경우

users.repo

decreasePoint = async (transaction, id, transferPoint) => {
  const userInfo = await this.usersModel.findOne(
    { attributes: ['id', 'point'], where: { id }, },
    { transaction }
  );
  // ...생략
  await userInfo.save({ transaction });
};

위와 같은 repository의 메소드가 있을 경우 

transaction을 어떻게 넘겨 처리해야 좋을지 궁금했는데

  test('users.repository increasePoint Method success', async () => {
    mockUsersModel.findOne = jest.fn(() => {
      return { id: 1, point: 10000, save: () => {} };
    });

    await usersRepository.increasePoint('transaction', 1, 10000);
    expect(mockUsersModel.findOne).toHaveBeenCalledTimes(1);
    expect(mockUsersModel.findOne).toHaveBeenCalledWith(
      { attributes: ['id', 'point'], where: { id: 1 }, },
      { transaction: 'transaction' }
    );
  });

일단 그냥 string으로 넣어 전달하는식으로 테스트를 했는데 일단 원하는 결과를 얻을 수 있었다.

이렇게 하는게 맞는지..?

 

덮어씌워져 버리는 parameter

service unit test code를 작성하면서

계속 반복되어 사용되는 파라미터에 대해 describe() 함수 들어가기 전에 세팅해놓고 진행을 했었다.

const userInfo = {
  id: 1,
  email: 'test@gmail.com',
  password: 'password',
  name: 'name',
  phone: '01012345678',
  address: 'address',
  isAdmin: false,
};

근데 service의 회원가입 메소드에서

userInfo.password = await this.encryptPassword(userInfo.password);

암호화된 값을 다시 도로 저장하는 코드가 있어서 그런지

createUser(회원가입) 메소드를 테스트한 코드 뒤로 

위의 공용으로 사용하는 userInfo 객체의 password가 암호화된 값으로 바꿔어져 있는 현상을 발견했다.

 

이 현상 때문에 뒤에 오는 모든 테스트 코드들이 영향을 받아 정상적인 테스트를 하지 못하게 되었었는데

userInfo 객체를 깊은 복사를 통해 하나 더 만들어서 

 

unit test code

const mockUserInfo = {
  id: 1,
  email: 'test@gmail.com',
  password: 'password',
  name: 'name',
  phone: '01012345678',
  address: 'address',
  isAdmin: false,
};

  // ...생략
  test('users.service createUser Method Fail - already registered', async () => {
    const userInfo = { ...mockUserInfo };

이런식으로 복사본을 하나 만들어 사용하는 방법을 사용했고 문제를 해결할 수 있었다.

 

login 메소드의 redis 사용

users.service

const redisClient = require('../utils/redis.util');

// ...생략
  login = async (email, password) => {
    try {
      // ...생략

      const accessToken = await TokenManager.createAccessToken(user.id);
      const refreshToken = await TokenManager.createRefreshToken();

      await redisClient.set(refreshToken, user.id);
      const TTL = parseInt(process.env.REDIS_REFRESH_TTL);
      await redisClient.expire(refreshToken, TTL);

// ...생략

로그인 하는 과정에서 refresh token을 redis 캐시 메모리에 저장하는 코드가 있는데

테스트 코드를 일반적으로 작성하면 redis에 실제로 refresh token을 저장해버리는 상황이 발생했다.

 

이 부분에서 어떻게 하면 좋을지 고민을 많이 했는데

테스트 코드 작성중 해당 클래스의 내부 함수에 대해

jest.fn() 하면 그 코드로 대체되버리는 문제를 겪었다가 해결한 뒤로 알게된 사실을 바탕으로

 

redisClient는 밖에서 모듈화한 객체를 불러온 것이기 때문에

해당 redisClient 함수들을 jest.fn() 으로 덮어씌워도 괜찮은 상황이 될 것 같아 진행해보았다.

 

unit test code

const redisClient = require('../../../src/utils/redis.util');
// ...생략
  test(`users.service login Method success`, async () => {
    const userInfo = { ...mockUserInfo };

    // ...생략

    redisClient.set = jest.fn(() => {});
    redisClient.expire = jest.fn(() => {});

    const response = await usersService.login(userInfo.email, userInfo.password);

    expect(response).toEqual({ code: 200, accessToken, refreshToken, message: '로그인 되었습니다.' });
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);
  });
redisClient.set = jest.fn(() => {});
redisClient.expire = jest.fn(() => {});

딱 이 부분만 보면 되는데

실제로 이렇게 세팅해놓으니 그 다음줄에서 

const response = await usersService.login(userInfo.email, userInfo.password);

실행되는 위 코드를 통해 redis에 refresh token이 저장되던 현상을 해결할 수 있었다.

또한 외부에서 불러오는 함수였기 때문에 다른 테스트 코드에도 영향을 미치지 않는 것을 확인할 수 있었다.

 

catch를 통해 리턴될 수 있는 여러 상황 모두 테스트 코드에?

users.service login 메소드

login = async (email, password) => {
  try {
    const user = await this.findOneByEmail(email);
    if (!user) {
      return { code: 400, message: '이메일 또는 비밀번호를 확인해주세요.' };
    }

    if (!(await this.checkPassword(password, user.password))) {
      return { code: 400, message: '이메일 또는 비밀번호를 확인해주세요.' };
    }

    const accessToken = await TokenManager.createAccessToken(user.id);
    const refreshToken = await TokenManager.createRefreshToken();

    await redisClient.set(refreshToken, user.id);
    const TTL = parseInt(process.env.REDIS_REFRESH_TTL);
    await redisClient.expire(refreshToken, TTL);

    return { code: 200, accessToken, refreshToken, message: '로그인 되었습니다.' };
  } catch (err) {
    console.log(err.message);
    return { code: 500, message: '로그인에 실패하였습니다.' };
  }
};
  1.  

보면

  • findOneByEmail()
  • checkPassword()  // 얘는 내부 함수라 제외
  • TokenManager.createAccessToken()
  • TokenManager.createRefreshToken()
  • redisClient.set()
  • redisClient.expire()

이 함수들을 통해 login 절차가 진행되고 있는데 여기중 하나에서 error가 생기면

밑의 catch문을 통해 

return { code: 500, message: '로그인에 실패하였습니다.' };

이 코드가 실행된다.

 

테스트 코드에 이 각각의 함수가 error를 내는 상황을 모두 구현했는데

이게 맞다고 생각하면서도 조금 의문이라 한번 남겨 보았다.

  test(`users.service login Method fail - error (1)`, async () => {
    const userInfo = { ...mockUserInfo };

    const afterPassword = await usersService.encryptPassword(userInfo.password);
    const findOneByEmailReturnValue = { id: 1, password: afterPassword };
    mockUsersRepository.findOneByEmail = jest.fn(() => {
      return findOneByEmailReturnValue;
    });

    TokenManager.createAccessToken = jest.fn(() => {
      return 'accessToken';
    });
    TokenManager.createRefreshToken = jest.fn(() => {
      return 'refreshToken';
    });

    redisClient.set = jest.fn(() => {});
    redisClient.expire = jest.fn(() => { throw new Error(); });

    const response = await usersService.login(userInfo.email, userInfo.password);

    expect(response).toEqual({ code: 500, message: '로그인에 실패하였습니다.' });
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);
  });

  test(`users.service login Method fail - error (2)`, async () => {
    const userInfo = { ...mockUserInfo };

    const afterPassword = await usersService.encryptPassword(userInfo.password);
    const findOneByEmailReturnValue = { id: 1, password: afterPassword };
    mockUsersRepository.findOneByEmail = jest.fn(() => {
      return findOneByEmailReturnValue;
    });

    TokenManager.createAccessToken = jest.fn(() => {
      return 'accessToken';
    });
    TokenManager.createRefreshToken = jest.fn(() => {
      return 'refreshToken';
    });

    redisClient.set = jest.fn(() => { throw new Error(); });

    const response = await usersService.login(userInfo.email, userInfo.password);

    expect(response).toEqual({ code: 500, message: '로그인에 실패하였습니다.' });
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);
  });

  test(`users.service login Method fail - error (3)`, async () => {
    const userInfo = { ...mockUserInfo };

    const afterPassword = await usersService.encryptPassword(userInfo.password);
    const findOneByEmailReturnValue = { id: 1, password: afterPassword };
    mockUsersRepository.findOneByEmail = jest.fn(() => {
      return findOneByEmailReturnValue;
    });
    
    TokenManager.createAccessToken = jest.fn(() => {
      return 'accessToken';
    });
    TokenManager.createRefreshToken = jest.fn(() => {
      throw new Error();
    });

    const response = await usersService.login(userInfo.email, userInfo.password);

    expect(response).toEqual({ code: 500, message: '로그인에 실패하였습니다.' });
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);
  });

  test(`users.service login Method fail - error (4)`, async () => {
    const userInfo = { ...mockUserInfo };

    const afterPassword = await usersService.encryptPassword(userInfo.password);
    const findOneByEmailReturnValue = { id: 1, password: afterPassword };
    mockUsersRepository.findOneByEmail = jest.fn(() => {
      return findOneByEmailReturnValue;
    });

    TokenManager.createAccessToken = jest.fn(() => {
      throw new Error();
    });

    const response = await usersService.login(userInfo.email, userInfo.password);

    expect(response).toEqual({ code: 500, message: '로그인에 실패하였습니다.' });
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);
  });

  test(`users.service login Method fail - error (5)`, async () => {
    const userInfo = { ...mockUserInfo };

    mockUsersRepository.findOneByEmail = jest.fn(() => {
      throw new Error();
    });

    const response = await usersService.login(userInfo.email, userInfo.password);

    expect(response).toEqual({ code: 500, message: '로그인에 실패하였습니다.' });
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
    expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);
  });

이렇게 각각 다 new Error를 내봄..

 

이미 테스트 코드 작성이 된 함수를 실행할 때마다 같은 테스트 코드 작성?

users.service.js

findOneByEmail = async (email) => {
  return await this.usersRepository.findOneByEmail(email);
};

unit test code

 

test('users.service findOneByEmail Method success', async () => {
  const userInfo = { ...mockUserInfo };

  const findOneByEmailReturnValue = 'test';
  mockUsersRepository.findOneByEmail = jest.fn(() => {
    return findOneByEmailReturnValue;
  });
  const result = await usersService.findOneByEmail(userInfo.email);

  expect(result).toEqual(findOneByEmailReturnValue);
  expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
  expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);
});

 

findOneByEmail() 함수에 대해 이미 체크를 하고 있는데 

service의 다른 메소드에서 findOneByEmail()을 사용할 때 마다

expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledTimes(1);
expect(mockUsersRepository.findOneByEmail).toHaveBeenCalledWith(userInfo.email);

계속 해줘야 하는게 맞나? .. 일단 해주긴 했음

728x90