목 함수

2022-05-28

mock

자 이번 시간에 mock 함수에 대해서 알아보겠습니다.

mock

목은 사전적인 의미로 `모의` `가짜` 라는 의미를 가지고 있습니다.

mockup

목업 이라는 표현도  사용하는데 `모형`, `모의` 이런 뜻이죠.

그러니까 mock 함수는 실제 함수를 구현한 것이 아니라,

그저 흉내만 낸 그러니까 테스트하기 위해서 만든 일종의 모형 이라고 생각하시면 되겠습니다.

예를 들어서 유저 db 에 접근해서 유저 리스트를 셀렉트 해오는 작업이 필요하다고 가정해 보겠습니다.

일단 이 경우에 작성 해야 될 코드가 상당히 많아 지겠죠.

이미 만들어져 있는 코드가 있으면 상관 없겠지만 새로 만들게 된다면 테스트를 위한 코드 보다 어쩌면 더 길어질 수도 있습니다.

그리고 저런 작업은 네트워크 환경이나 db의 상태 등 외부 요인에 의해 결과가 달라질 수 있습니다.

테스트를 하기 위해서는 동일한 코트는 동일한 결과를 내야겠죠.

자 한번 사용해보겠습니다.

mock 사용

fn.test.js

const mockFn = jest.fn();

mockFn();
mockFn(1);

jset.fn() 으로 mock 함수를 만들수 있습니다.

이렇게 만들고 호출을 해 보겠습니다.

이렇게 인수를 넘길수도 있구요.

이걸로 뭘 할 수 있을까요?

이렇게 만든 목 함수에는 mock이 라는 프로퍼티(property)가 있는데, 이 안에는 calls라는 배열이 있습니다 한번 찍어 볼게요.

mock.calls

fn.test.js

const mockFn = jest.fn();

mockFn();
mockFn(1);

test('dd', () => {
  console.log(mockFn.mock.calls)
  expect('dd').toBe('dd');
});

의미없는 테스트를 하나 작성하구요.

이렇게 목 이라는 프로퍼티(property) 안에 calls가 있습니다.

test

# npm test

PASS ./fn.test.js
v dd ( 7 ms )

console.log
  [ [], [ 1 ] ]

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshot: 0 total
Time: 1.284 s

이런 배열이 나왔어요.

여기서 목 함수의 강점이 잘 드러나는데, 이 .mock 프로퍼티에는 호출되었던 어떤 값들이 고스란히 저장되어 있다는 점입니다.

  • 몇번 호출 됐는지?
  • 어떤 인수가 전달 되었는지?

알기 때문에 이를 활용할 수 있습니다.

calls 이걸로 알 수 있는 내용은 두 가지입니다.

  • 함수가 총 몇 번 호출 되었는가?
  • 호출될 때 전달된 인수는 무엇인가?

작성을 한번 해볼게요.

fn.test.js

const mockFn = jest.fn();

mockFn();
mockFn(1);

test('함수는 2번 호출됩니다.', () => {
  expect(mockFn.mock.calls.length).toBe(2);
});

test('2번째로 호출된 함수에 전달된 첫번째 인수는 1입니다.', () => {
  expect(mockFn.mock.calls[1][0]).toBe(1);
});

calls의 길이는 호출된 횟수 겠죠.

그리고 이 배열 내부에는 전달된 인수의 값이 배열로 들어있습니다.

작성한 코드를 테스트 해보겠습니다.

test

# npm test

PASS ./fn.test.js
v 함수는 2 호출됩니다. ( 7 ms )
v 2번째로 호출된 함수에 전달된 첫번째 인수는 1입니다. 

Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshot: 0 total
Time: 0.864 s, estimated 1 s

둘 다 통과 했어요.

fn.test.js

const mockFn = jest.fn();

function forEachAdd1(arr) {
  arr.forEach(num => {
    mockFn(num + 1);
  });
}

forEachAdd1([10, 20, 30]);

test("함수 호출은 3번 됩니다.", () => {
  expect(mockFn.mock.calls.length).toBe(3);
});

test("전달된 값은 11, 21, 31 입니다.", () => {
  expect(mockFn.mock.calls[0][0]).toBe(11);
  expect(mockFn.mock.calls[1][0]).toBe(21);
  expect(mockFn.mock.calls[2][0]).toBe(31);
});

이번에는 숫자가 들어 있는 배열을 반복하면서 1 증가 시켜 준 값을 콜백 함수에 전달 해주는 forEachAdd1() 이라는 함수를 만들어보겠습니다.

여기에 들어갈 함수는 아직 만들지 않았습니다.

이럴 때 시간을 들여서 함수를 작성하지 말고 빠르고 간단한 테스트를 위해서 mock 함수를 활용해 보겠습니다.

이렇게 mock 함수를 만들고, 이렇게 콜백 함수로 전달을 해줍니다.

실재 사용을 해보겠습니다.

함수호출은 3번 되겟죠.

전달된 값은 1씩 증가하는 값입니다.

이렇게 만들고 테스트를 해보겠습니다.

test

# npm test

PASS ./fn.test.js
v 함수 호출은 3 됩니다. ( 2 ms )
v 전달된 값은 11, 21, 31 입니다.

모두 통과 됐어요.

자 지금 이 콜백 함수를 굳이 새로 만들어서 전달해 보지 않아도 mock() 함수 를 이용해서 forEachAdd1 함수는 잘 동작한다는 걸 알 수 있죠.

fn.test.js

const mockFn = jest.fn(num => num + 1);

mockFn(10);
mockFn(20);
mockFn(30);

test("함수 호출은 3번 됩니다.", () => {
  console.log(mockFn.mock.results);
  expect(mockFn.mock.calls.length).toBe(3);
});

이제 어떤 값을 리턴하는 함수가 필요하다고 합시다.

숫자를 받아서 +1을 해주겠습니다.

이렇게 test.fn 의 첫 번째 인수로 함수를 전달해줍니다.

그리고 한번 호출해볼게요.

마찬가지로 3번 호출 되겠죠.

mock.results

이번에는 results 라는 값을 한번 찍어보겠습니다.

test

# npm test

PASS ./fn.test.js
v 함수 호출은 3 됩니다.

console.log
  [
    { type: 'return', value: 11 },
    { type: 'return', value: 21 },
    { type: 'return', value: 31 },
  ]

이 results 에는 리턴된 값이 배열로 들어있습니다.

이렇게 1씩 증가된 값이 리턴된 걸 확인할 수 있습니다.

fn.test.js

const mockFn = jest.fn(num => num + 1);

mockFn(10);
mockFn(20);
mockFn(30);

test("10에서 1 증가한 값이 반환된다.", () => {
  expect(mockFn.mock.results[0].value).toBe(11);
});

test("20에서 1 증가한 값이 반환된다.", () => {
  expect(mockFn.mock.results[1].value).toBe(21);
});

test("30에서 1 증가한 값이 반환된다.", () => {
  expect(mockFn.mock.results[2].value).toBe(31);
});

테스트 코드는 이렇게 작성할 수 있겠네요.

test

# npm test

PASS ./fn.test.js
v 10에서 1 증가한 값이 반환된다.
v 20에서 1 증가한 값이 반환된다.
v 30에서 1 증가한 값이 반환된다.

테스트는 모두 통과 했죠.

mockReturnValue

사용할 때 마다 각각 다른 값을 리턴해 줄 수도 있습니다.

fn.test.js

const mockFn = jest.fn();

mockFn
  .mockReturnValueOnce(10)
  .mockReturnValueOnce(20)
  .mockReturnValueOnce(30)
  .mockReturnValue(40);
  
mockFn();
mockFn();
mockFn();
mockFn();

test("dd", () => {
  console.log(mockFn.mock.results);
  expect("dd").toBe("dd");
});

이때는 mockReturnValue 을 사용해 주는데요.

중간을 리턴 값을 바꾸려는 once를 붙여 주면 됩니다.

마지막에는 이렇게 once를 빼 주시구요.

이렇게 한번 찍어 볼게요.

test

# npm test

PASS ./fn.test.js
v dd

console.log
  [
    { type: 'return', value: 10 },
    { type: 'return', value: 20 },
    { type: 'return', value: 30 },
    { type: 'return', value: 40 }
  ]

이렇게 나오자 확인해보면 10 20 30 40 이 리턴된걸 알 수 있습니다.

이걸 활용해서 1부터 5까지 받아서 홀수 만 리턴하는 함수를 한번 작성해 보겠습니다.

fn.test.js

const mockFn = jest.fn();

mockFn
  .mockReturnValueOnce(true)
  .mockReturnValueOnce(false)
  .mockReturnValueOnce(true)
  .mockReturnValueOnce(false)
  .mockReturnValue(true);
  
const result = [1, 2, 3, 4, 5].filter(num => mockFn(num));

test("홀수는 1,3,5", () => {
  expect(result).toStrictEqual([1, 3, 5]);
});

여기서 이 콜백 감수가 홀수 인지 짝수 인지 판별 해주는 역할을 해야 되는데, 뭐 사실 굉장히 단순한 로직이긴 한데, 지금 당장 코드를 만들 형편이 되지 않는다면, 이 mock() 함수를 쓰는 거에요.

mock() 함수는 단순 true와 false를 번갈아가면서 리턴해 주도록 하겠습니다.

배열을 확인할 때는 toBe()가 아니라 toStrictEqual()를 사용하셔야 됩니다.

원래는 이 부분(num => mockFn(num))에 다가 홀수 인지 짝수 인지 판별해 줄 수 있는 함수를 작성을 해야 되는데,

일단은 목 함수를 사용하고 어떤 숫자가 넘어오던지 그냥 순서대로 true false, true false 이렇게 번갈아 하면서 리턴을 해주고 있습니다.

실행을 해보면 1 3 5만 이 부분이 true가 될 것이기 때문에, result는 1, 3, 5 가 될 겁니다.

test

# npm test

PASS ./fn.test.js
v 홀수는 1,3,5 (1 ms)

잘 나오네요.

mock() 함수 대신에 실제에 code를 작성하시면 되겠습니다.

mockResolvedValue

mockReturnValue()로 말고 mockResolvedValue()로 사용하면 비동기 함수를 흉내낼수도 있습니다.

fn.test.js

const mockFn = jest.fn();

mockFn
  .mockResolvedValue({ name : "Mike" });
  
const result = [1, 2, 3, 4, 5].filter(num => mockFn(num));

test("받아온 이름은 Mike", () => {
  mockFn().then(res => {
    expect(res.name).toBe("Mike");  
  });
});

test

# npm test

PASS ./fn.test.js
v 받아온 이름은 Mike

이렇게 비동기 함수 처럼 then()을 이용해서 여기서 리턴해 주는 값을 비교해 볼 수 있습니다.

외부 코드를 활용한 테스트

이번에는 이렇게 여기에 있는 외부 코드를 활용한 테스트가 필요하다고 가정을 해보겠습니다.

fn.js

const fn = {
  add: (num1, num2) => num1 + num2,
  createUser: name => {
    console.log("실제로 사용자가 생성되었습니다.");
    return {
      name,
    };
  },
  connectUserDb : () => {
    return new Promise(res => {
      setTimeout(() => {
        res({
          name: "Mike",
          age: 30,
          gender: "male",
        });
      }, 500);
    });
  },
  disconnectDb: () => {
    return new Promise(res => {
      setTimeout(() => {
        res();
      }, 500);
    });
  },
  connectCarDb : () => {
    return new Promise(res => {
      setTimeout(() => {
        res({
          brand: "bmw",
          name: "z4",
          color: "red",
        });
      }, 500);
    });
  },
  disconnectCarDb: () => {
    return new Promise(res => {
      setTimeout(() => {
        res();
      }, 500);
    });
  }
};

module.exports = fn;

유저를 생성하는 코드를 테스트 해보고 싶은데, 테스트할 테마다 실제로 유저가 생겨 버리면 곤란하겠죠.

그렇다고 테스트가 끝날 때마다 롤백 해주는 것도 상당히 번거로울 겁니다.

이럴 때는 모킹 모듈이 라는 것을 쓰면 됩니다.

이렇게 이름을 받아서 유저 객체를 리턴 해주는 함수를 만들었습니다.

실제로는 유저 db에 유저를 생성해 주는 함수 라고 가정을 하겠습니다.

실제로 한번 사용을 해 볼게요.

fn.test.js

const fn = require("./fn");

test("유저를 만든다", () => {
  const user = fn.createUser("Mike");
  expect(user.name).toBe("Mike");
});

test

# npm test

PASS ./fn.test.js
v 유저를 만든다

console.log
  실제로 사용자가 생성되었습니다.

테스트를 통과 있고 실제로 유저가 생겨버렸습니다.

모킹 모듈

이런 경우 다시 db 접속해서, 방금만든 테스트 유저는 삭제해야 되겠죠.

fn.test.js

const fn = require("./fn");

jest.mock("./fn");

fn.createUser.mockReturnValue({ name: "Mike" });

test("유저를 만든다", () => {
  const user = fn.createUser("Mike");
  expect(user.name).toBe("Mike");
});

이럴때 mock() 이란걸 사용합니다.

그리고 jest.mock() 으로 fn을 모킹 모듈로 만들어 줍니다.

이제 fn.createUser()를 사용할 텐데 mockReturnValue()를 통해서 객체를 리턴하도록 하겠습니다.

이렇게 작성하면 실제 fn.createUser() 는 호출되지 않습니다.

다만 이 객체를 반환 해주는 mock() 함수가 동작할 뿐이에요.

test

# npm test

PASS ./fn.test.js
v 유저를 만든다

수행을 한번 해보면 테스트는 통과했고 아까 보였던 로그는 찍히지 않습니다.

실제로 유저는 생성 되지는 않았다는 거죠.

이렇게 모킹 모듈를 이용하면 실제 이 함수가 동작하지 않습니다

실제 유저가 생성 되지는 않았다는 거죠.

유용한 Mock Matcher

마지막으로 유용한 Matcher들 소개해 드리고 마치겠습니다

fn.test.js

jest.mockFn = jest.fn();

mockFn(10, 20);
mockFn();
mockFn(30, 40);

test("한번 이상 호출?", () => {
  expect(mockFn).toBeCalled;
});
test("정확히 세번 호출", () => {
  expect(mockFn).toBeCalledTimes(3);
});
test("10이랑 20 전달받은 함수가 있는가?", () => {
  expect(mockFn).toBeCalledWith(10, 20);
});
test("마지막 함수는 30이랑 40 받았음?", () => {
  expect(mockFn).lastCalledWith(30, 40);
});
  • toBeCalled() 은 한 번이라도 호출 됐으면 통과 됩니다.

  • toBeCalledTimes() 는 정확한 호출 횟수를 의미합니다. 지금 세번 호출됐기 때문에 3이 되겠죠.

  • toBeCalledWith() 는 인수로 어떤 값들을 받았는지 체크하고,

  • lastCalledWith() 는 인수를 체크하는 건 동일하지만 마지막으로 실행된 함수의 인수만 체크합니다.

test

# npm test

PASS ./fn.test.js
v 한번 이상 호출?
v 정확히 세번 호출
v 10이랑 20 전달받은 함수가 있는가?
v 마지막 함수는 30이랑 40 받았음?

모두 통과 하죠.

toBeCalledWith()에 이 부분을 30 이랑 40 으로 바꿔도 통과하게 됩니다.

하지만 lastCalledWith()를 10이라는 20으로 바꾸면 실패하게 되죠.

마지막은 30과 40을 전달 했기 때문입니다.

이번시간은 목 함수에 대해서 알아봤습니다.

특정 기능의 집중하는 가짜 함수를 적절히를 활용하면서 효율적인 코딩 및 테스트 진행하시길 바랍니다.

results matching ""

    No results matching ""

    99 other / uml

    04 react / JSX