카테고리 없음

TypeScript와 Express를 Jest로 테스트

나는 나야 2024. 4. 16. 23:03

예전에 일반적으로 빠르게 관리자 인증/인가를 모두 완성하고, 모두가 알고있는 포스트맨으로 실제로 원하는 값이 나오는지 테스트를 몇 번이나 하면서 API 개발을 완성했었습니다.

하지만, 이러한 코드는 코드를 수정할 때마다 사이드 이펙트(Side-Effect)가 없는지 다시 포스트맨으로 테스트를 해야하는 상황이었습니다. 이러한 방법으로 테스트를 진행하면서 흘려보낸 많은 시간이 있었습니다.


또한, Product Code로 만들기 위해서는 작성한 코드에 대해서 보장이 돼 있어야한다고 생각했습니다. 이러한 생각과 과정으로 인해 테스트를 진행했습니다. 기존, Spring Boot로는 테스트 코드를 몇 번 작성해봤었지만, Typescript, Node.js, Express, Prisma를 사용하면서 테스트 코드를 짜는 것은 조금 다르기도 했습니다.

그래서 이번에 쓸 주제는 Jest 라는 프레임워크 이자, 어떤 부분을 테스트 해야하는지 얘기해 볼 생각입니다.

얘기해 볼 것들은 테스트 할 때, 많은 시간을 썼던 테스트들, 그리고 왜 그랬었는지, 마지막으로 테스트를 진행할 때 집중해야하는 것들에 대해서 기술하려고 합니다.

먼저 가장 오래걸렸었던 테스트는 '로그인' 테스트인데, 로그인 로직에 다른 서버에 Axios를 통해 통신하는 것을 Mocking 하는 것과 Redis를 mocking해서 테스트 용으로 진행할 때 대략.. 3시간 정도 걸렸었던 것 같습니다. 
실제로 Redis Mocking을 처음 해보기도 했고, Axios 또한, 처음 Mocking하는 것들이었습니다.

우선적으로 Axios Mocking을 어떻게 해결했는지 말씀드리겠습니다. 
기존의 Axios 요청은 아래와 같았습니다.

const axiosResponse = await axios({
    method: "get",
    url: `${process.env.SERVER_URL}/info`,
    params: {
        site: site,
    },
});

const corporation = axiosResponse.data;

if (!corporation) {
    throw new NotFoundCorporation();
}

return corporation;



간단하게 Axios 통신 요청을 처리했고, 동기식으로 진행하기 위해 await을 걸어 데이터를 받아오는 로직이었습니다.

이를 Mocking하기 위해, Jest 공식문서에는 아래와 같이 템플릿으로 나와있었습니다.
정확하게 따라서 진행을 했지만, 결과는 undefined 라는 결괏값만이 나왔습니다.

처음에는 어떻게든 초록불을 보기 위해 함수로 만들어서 가짜함수의 결괏값을 받아서 진행했었습니다.

getSiteDataAxios = async (site: number) :Promise<SiteData> => {

    const axiosResponse = await axios({
        method: "get",
        url: `${process.env.SERVER_URL}/info`,
        params: {
            site: site,
        },
    });

    const corporation = axiosResponse.data;

    if (!corporation) {
        throw new NotFoundCorporation();
    }

    return corporation;
}


를 통해,

corp.getSiteDataAxios = jest.fn().mockResolvedValue(1)


하지만, 이러한 해결책은 진정한 해결이 될 수 없습니다. 그래서 이를 해결하기 위해서 간단한 Axios 테스트만을 위해서 테스트를 진행했고,
총 1시간이라는 시간을 거쳐 결과는 성공적이었습니다.

우선, Axios 통신을 진행하는 것은 크게 두가지 방법이 있습니다.

1번 방법

axios({
method : "get",
url : ~~
})



2번 방법

axios.get(~~~)


방법 중 두 번째 방법을 사용해 원하는 결괏값을 볼 수 있었습니다.

// 작성한 테스트 코드
axiosTest = async (corporation: string) => {
    const response = await axios.get(
        `${process.env.SERVER_URL}/info`,
        {
        params: {
            corporation: corporation,
        },
    });

    if (!response) {
        throw new Error("값이 없음");
    }

    return response.data;
};

 

이와 같이 결괏값이 나온 이유에 대해서는 이후에 알게 되었지만, mocking은 axios.get을 모킹한 것이지 axios({method : get})을 모킹한게 아니기 때문입니다!


두 번째로는 Redis Mocking이었습니다. 

// Redis 모킹 설정
jest.mock('redis', () => jest.requireActual('redis-mock'));

const redis = require('redis');
const client = redis.createClient();

describe('Redis', () => {
    it('should set user info in Redis', (done) => {
        const userInfo = { id: 1, name: 'John Doe' };
        const token = 'some-token';
        client.set(token, JSON.stringify(userInfo), () => {
            client.get(token, (err : Error | null, result : string | null) => {
                expect(result).toEqual(JSON.stringify(userInfo));
                done();
            });
        });
    });
});


간단하게 Mocking한 Redis의 동작이 올바른지 테스트를 진행했습니다.

그리고 '로그인' 테스트에 set 메서드를 사용할 수 있도록 아래처럼 Mocking 하였습니다.

jest.mock("redis", () => ({
    createClient: () => ({
        connect: jest.fn().mockResolvedValue("Redis connected"),
        set: jest.fn().mockResolvedValue(true),
        get: jest.fn().mockResolvedValue("mockedRefreshToken"),
        disconnect: jest.fn().mockResolvedValue("Redis disconnected"),
    }),
}));


이러한 진행을 통해 무사히(?) 3시간 만에 하나의 테스트를 진행할 수 있었습니다.

두 번째 포인트로 테스트하는 목적에 집중하자 입니다.
종종 테스트에 몰두하다 보면, 필자가 진정으로 원하는 테스트가 무엇인지 잊게되고 기존에 given으로 주었던 값들을 그대로 복사해서 테스트의 본질을 흐리는 경우가 있습니다.
예를 들면, 아래와 같은 경우 말이죠.

it("비밀번호 변경 시 기존의 비밀번호와 다르면 False를 반환한다.", async () => {
    // given
    const signInfo = {
        login_id: "test0",
        password: "testPassword",
        name: "kim",
        phone_number: "010-000-0000",
        corporation: "Company",
    };

    managerService.getSiteIdAxios = jest.fn().mockResolvedValue(1);
    await managerService.signUpMapper(signInfo);
    const managers = await database.managers.findMany();
    const manager = managers[0];

    // when    
    const isCorrect = await managerService.comparePassword("testFail", manager.password);
 
    // then
    expect(isCorrect).toBeFalsy();
})



어떠신가요? 작성해놓은 글을 보고 어떤 테스트구나는 알겠는데 정확하게 어떤 메서드를 테스트 하는 것인지 헷갈릴 수 있습니다.

필자의 목적은 managerService.comparePassword 이 메서드를 테스트 함으로써 보장하려고 했었습니다.

작성해놓고 생각하면, 굳이 manager의 생성하는 부분은 필요가 없습니다. 즉, 코드를 정리하면

it("비밀번호가 다르면 비밀번호 비교시 False를 반환한다.", async() => {
    const test = "TEST";
    const hashedString = await managerService.hashPassword(test);
    const checkedPassword = await managerService.comparePassword("test", hashedString);

    expect(checkedPassword).toBeFalsy();
})



매우 간단하게 변했습니다. 또한 직관적으로 보입니다.

이렇듯 테스트할 때에도 '목적'에 집중을 해야한다는 생각이 강력하게 드는 테스트 코드였습니다.

테스트를 진행하면서 많은 어려움이 있었습니다. 하나의 서비스의 테스트 코드를 모두 작성해도 말이죠.

필자처럼, 처음해보는 Jest에 허둥지둥하거나 하나의 테스트 코드를 작성하는데 3시간이 걸릴 일도 많을 수 있습니다. 처음부터 잘되는 것은 잘 없기 때문이지요. 이 글은 테스트를 진행하면서 겪었던 오류와 들었던 생각을 정리했습니다. 시간이 되고 가능하다면, 테스트 코드를 통해 스스로 작성한 코드를 보장할 수 있도록 하면 좋겠습니다.