Jest Mocking
Jest Mocking

Jest Mocking

Created
Mar 25, 2022 08:33 AM
Tags
jest
test
mock
Prisma
author
notion image

module mocking pattern

아래와 같이 libs 모듈에 mqttClient가 담겨있고, 테스트할 함수에 테스트 환경에서는 실행되면 안되는 MQTT 클라이언트가 있다고 가정하면
src
├─┬ libs
│ └─┬ mqtt
│   ├── mqttClient.ts
│   └─┬ __mocks__
│     └── mqttClient.ts
├── index.ts
└─┬ process
  ├─┬ __tests__
  │ └── process.spec.ts
  └── index.ts

모듈 mock 함수를 만드는 작업은 아래와 같이 같은 레벨에서 import하는 함수 파일과 동일한 파일을 __mocks__ 하위에 같은 이름으로 만든다.
아래 mock함수는 사용하는 MQTT Client를 커스터마이징 하여 함수 구현 모듈을 만들어 사용하였다.
// src/libs/mqtt/__mocks__/mqttClient.ts

const MQTTClientMock = jest.fn().mockImplementation(() => ({
  client: jest.fn().mockImplementation(() => ({
    subscribe: jest.fn(),
    on: jest.fn(),
    publish: jest.fn(),
  })),
  listening: jest.fn(),
  subscribe: jest.fn(),
  removeRetain: jest.fn(),
  publish: jest.fn(),
}));

export default MQTTClientMock;

export const mqttClient = new MQTTClientMock();

테스트할 함수는 mqttClient를 사용하고 있으며, 이 함수process의 코드는 테스트 되어야 하고, mqttClient는 Mocking 되어야 한다.
// src/process/__tests__/process.spec.ts

import { process } from "../process"

jest.mock("libs/mqtt/mqttClient");

describe("process test", () => {
  it("should return normal result", () => {
    const result = process();
    expect(result).toEqual(expectaionResult);
  })
})

하지만 mock 함수의 결과값을 활용하여 process코드에 값이 있어야 하는 경우 mockImplementation을 수행할 수 도 있다.
// src/process/__tests__/process.spec.ts

import { process } from "../process";
import { mqttClient } from "libs/mqtt/mqttClient";

jest.mock("libs/mqtt/mqttClient");

describe("process test", () => {
  it("should return normal result", () => {
// mqttClient.publish 함수를 실행한 결과는 1이 되어야할 경우
    (mqttClient.publish as jest.Mock).mockResolvedValue(1);
    const result = process();
    expect(result).toEqual(expectaionResult);
  })
})

module mocking pattern (extended)

사용하는 모듈의 메서드가 꽤 많아 모든 mock 메서드를 만들어야 할 경우 모듈의 타입을 참조하여 Mocking 한다.
예를들어 prisma 클라이언트를 mocking 하는 경우,
src
├─┬ libs
│ └─┬ prisma
│   ├── prismaClient.ts
│   └─┬ __mocks__
│     └── prismaClient.ts
├── index.ts
└─┬ process
  ├─┬ __tests__
  │ └── process.spec.ts
  └── index.ts

jest-mock-extended 모듈을 사용하여 Mocking 한다.
// src/libs/prisma/__mocks__/prismaClient.ts

import { PrismaClient } from "@prisma/client";
import { mockDeep, mockReset, DeepMockProxy } from "jest-mock-extended";
import prismaOrigin from "../prisma";

jest.mock("../prisma", () => ({
  __esModule: true,
  default: mockDeep<PrismaClient>(),
}));

beforeEach(() => {
  mockReset(prisma);
});

export type PrismaMock = DeepMockProxy<PrismaClient>;

export const prisma = prismaOrigin as unknown as DeepMockProxy<PrismaClient>;

export default prisma;

여기서 주목해야할 부분은 jest.mock 과 동시에 default에 mockDeep 함수로 타입스크립트 타입을 제네릭으로 전달한다는 점.
그리고 모든 테스트를 수행하기전에 Mocking 된 Prisma를 리셋한다는 점이다.
Mocking된 prisma는 실제 반환해야할 DB값을 전달하여 여러 데이터 케이스에 따라 테스트가 될 수 있도록 고려해야한다.
import { PrismaMock } from "libs/prisma/__mocks__/prismaClient";
import { prisma } from "libs/prisma/prismaClient";
import { process } from "../process";

jest.mock("libs/prisma/prismaClient");

describie("query user findUnique process test", () => {
  it("should resolve normal result", async () => {
    (prisma as unknown as PrismaMock)
      .user
      .findUnique
      .mockResolvedValue(sampleUser);
    const result = process();
    expect(result).toEqual(expectedResult);
  })
})

prisma 클라이언트에서 에러가 발생하는 경우를 Mocking할땐, mockRejectedValue 함수를 호출한다.

custom module mocking pattern

case 1: export default jest.mockmockImplementation

src
├─┬ libs
│ └─┬ module
│   └── module1.ts
├── index.ts
└─┬ process
  ├─┬ __tests__
  │ └── process.spec.ts
  └── index.ts

// src/libs/module/module1.ts

export default function module1() {
  return { error: false, message: null, data: "hello real world" }
}

// src/process/index.ts

import moduleDefault from "libs/module/module1";

export function process() {
  const data = moduleDefault();
  return data;
}

default export된 함수의 경우 jest.mock으로 경로를 적어주면 자동으로 Mocking이 된다. auto mocking의 경우에도 가상의 값을 리턴하여 함수가 오류가 발생하지 않도록, 그리고 케이스를 다양하게 구성하여 테스트를 한다.
// src/process/__tests__/process.spec.ts

import { process } from "../process";
import moduleDefault from "libs/module/module1";

describe("custom module mock", () => {
  beforeEach(jest.mockClear);
  it("should return normal value", () => {
    jest.mock("libs/module/module1");
    (moduleDefault as jest.Mock).mockReturnValue({
      error: false,
      message: null,
      data: "hello test world",
    });
    const result = process();
    expect(result).toEqual(expectedResult);
  })
})

case 2: named export jest.spyOn

src
├─┬ libs
│ └─┬ module
│   └── module1.ts
├── index.ts
└─┬ process
  ├─┬ __tests__
  │ └── process.spec.ts
  └── index.ts

// src/libs/module/module1.ts

export function module1() {
  return { error: false, message: null, data: "hello real world" }
}

// src/process/index.ts

import { module1 } from "libs/module";

export function process() {
  const data = module1();
  return data;
}


jest.spyOn 함수는 테스트하려는 함수 내부의 Mocking 하고자 하는 함수를 명시하고 Mocking한 리턴값을 적는다. (TypeScript 작동하여, 실제 맞는 타입의 가상의 값을 주어야 함)
또한 spyOn을 하려는 함수는 Mocking을 해야하며 Mocking후 실제 함수 결과를 리턴하도록 jest.requireActual을 사용하여 Mocking 한다.
// src/process/__tests__/process.spec.ts

import { process } from "../process";
import * as module from "libs/module";

jest.mock("libs/module", () => ({
  __esModule: true,
  ...jest.requireActual("libs/module"),
}));

describe("custom module mock", () => {
  it("should return normal value", () => {
    const spy = jest.spyOn(module, "module1").mockReturnValue({
      error: false,
      message: null,
      data: "hello test world",
    });
    const result = process();
    expect(result).toEqual(expectedResult);
    spy.mockRestore();
  })
})