TDD/Jest

[TDD/Jest] 모킹하기 Mocking

y0ngha 2021. 12. 29. 23:42

Mocking

코드가 의존하는 부분을 가짜(mock)로 대체하는 기법이다. 
테스트하려는 코드가 의존하는 부분을 직접 생성하기가 부담스러운 경우 mocking이 많이 사용된다.

예를 들어, 데이터베이스에서 데이터를 삭제하는 코드에 대한 단위 테스트를 작성할 때, 실제 데이터베이스를 사용한다면 여러가지 문제점이 발생할 수 있기 때문에 모킹을 사용한다.

 

모킹을 하는데에는 여러 방법이 있다. (모듈 단위를 모킹하거나, 함수만 모킹하거나.. 등)

 

jest.fn();

일반적으로 함수를 모킹할 때 사용된다.

// 정의
const mockFunction = jest.fn();

// 사용
mockFunction("y0ngha");

이 mockFunction을 실제 함수를 호출하듯 사용할 수 있고, 리턴 값에 대해서도 정의해줄 수 있다.

// 정의
const mockFunction = jest.fn();

// 리턴값 설정
mockFunction.mockReturnValue("Hello!");

// 사용
console.log(mockFunction()); // 결과 : Hello!

이 외에도 Promise에서 resolve한 비동기 함수 또한 만들 수 있다.

// 정의
const mockFunction = jest.fn();

// 리턴값 설정
mockFunction.mockResolvedValue("Hello!");

// 사용
mockFunction.then((result) => {
    console.log(result); // 결과 : Hello!
})

함수를 모킹하고 테스트 코드 내에서 함수를 즉석으로 구현해버릴 수도있다.

// 정의
const mockFunction = jest.fn();

// 함수 구현
mockFunction.mockImplementation((name) => {
    return `Hello ${name}`;
});

// 사용
console.log(mockFunction("y0ngha")); // 결과 : Hello y0ngha

 

가짜 함수는 자신이 어떻게 호출되었는지를 모두 기억한다.

const mockFunction = jest.fn();

mockFunction("y0ngha");
mockFunction(["gildong", "chulsu"]);

expect(mockFunction).toBeCalledTimes(2);
expect(mockFunction).toBeCalledWith("y0ngha");
expect(mockFunction).toBeCalledWith(["gildong", "chulsu"]);

jest.spyOn();

특정 객체에 있는 함수의 구현에 대해 호출 여부, 어떻게 호출이 되었는지, 호출 결과는 무엇인지에 대해 도출할 때 사용된다.

const calculator = {
  add: (a, b) => a + b,
};

const spyFunction = jest.spyOn(calculator, "add"); // calculator 객체에서 add 함수를 빼옴

const result = calculator.add(1, 1);

expect(spyFunction).toBeCalledTimes(1); // 함수가 1번 실행되었는지 체크
expect(spyFunction).toBeCalledWith(1, 1); // 함수에 대한 인자 값이 1, 1인지 체크
expect(result).toBe(2); // result에 대한 값이 2인지 체크

예를 들어 비즈니스 로직에서 axios를 이용해 Network 통신으로 데이터를 가져오는 것을 캐치하고 싶을 때 spyOn을 사용한다.

데이터가 정상적으로 가져와졌는지, 호출한 주소는 무엇인지에 대해 모두 검사할 수 있다.

jest.mock();

외부 패키지나, 본인이 직접 만든 모듈을 모킹할 때 사용된다.

본인이 직접 만든 모듈이 메일을 보내거나 SMS를 보내는 메시지 서비스라고 가정하고, 이 모듈을 사용하는 곳에 대해 테스트 하기 위해서는 jest.mock(); 을 사용하지 않으면 메시지 서비스 내에 있는 함수를 모두 jest.fn();, jest.spyOn() 으로 가져와야 한다.

/** messageService.ts */
export function sendMail(title: string, message: string): boolean {
    // 메일 보내는 로직
}

export function sendSMS(content: string): boolean {
    // 메시지 보내는 로직
}
/** registerService.ts */
import { sendMail, sendSMS } from "./messageService";

export function register(id: string, password: string): boolean {
    const message: string = "회원가입을 해주셔서 감사합니다.";
    const mailTitle: string = "서비스 가입이 완료되었습니다.";
    
    sendMail(mailTitle, message);
    sendSMS(message);
    
    return true;
}

 

위 registerService.ts 를 테스트하는 코드를 작성하기 위해서는 sendMail과 sendSMS 함수를 구현해주어야 한다.

함수의 갯수가 많아지면 많아질수록 모킹을 많이 진행해야하고, 이로 인해 번거로운 작업을 계속 진행해야 한다.

import * as messageService from "./messageService";

messageService.sendEmail = jest.fn();
messageService.sendSMS = jest.fn();

const sendMail = messageService.sendMail;
const sendSMS = messageService.sendSMS;

beforeEach(() => {
  sendMail.mockClear();
  sendSMS.mockClear();
});

describe("user register service", () => {
    it("register user", () => {
        ...
        expect(sendMail).toBeCalledWith("회원가입을 해주셔서 감사합니다.")
    })
});

만약 messageService에 있는 함수가 2개가 아니라, 10개.. 100개가 넘어간다면 이 모든 함수를 모킹해주느라 시간이 더 들 것이다.

이럴 때 우리는 모듈 단위로 모킹을 할 수 있다.

import { sendMail, sendSMS } from "./messageService";

jest.mock("./messageService");

beforeEach(() => {
  sendMail.mockClear();
  sendSMS.mockClear();
});

describe("user register service", () => {
    it("register user", () => {
        ...
        expect(sendMail).toBeCalledWith("회원가입을 해주셔서 감사합니다.")
    })
});

mock(); 을 쓸 때와 fn(); 을 쓸 때 import 하는 부분이 서로 다른 것을 볼 수 있는데, 역직렬화 해서 가져올 경우 read-only 특성을 갖고 있기 때문에 jest.fn(); 으로 치환할 수 없다.

때문에 조금은 억지스럽지만 객체로 가져오느라 역직렬화를 안쓴 것이다.

 

하지만 mock(); 을 쓰게 된다면 위와 같이 간편하게 가져올 수 있다.

이를 이용해 모듈단위로 모킹하여 편하게 사용할 수 있으며, react의 경우에 react-redux와 같이 외부 패키지에서 불러올 때(useSelector...) 모듈단위로 모킹하여 사용된다.