Vue 3.0/03.SFC와 Vite

[vue 3] 06. vitest를 이용한 단위 테스팅 3

  • -

이번 포스트에서는 가짜 객체인 mock을 이용한 테스트 방식에 대해서 살펴보자.

 

시간 기반 테스팅과 가짜 객체의 활용

 

시간 기반 테스트

진짜 코드들만도 관리하기 힘든데 왜 우리는 가짜 객체까지 활용하면서 테스팅을 해야할까? 만약 일과 시간(09:00 ~ 18:00) 에만 동작해야 하는 함수가 있다고 생각해보자.

const purchase = () => {
    const currentHour = new Date().getHours()
    const [open, close] = [9, 18]

    return currentHour > open && currentHour < close
}

위 함수가 제대로 동작하는지 검증하기 위해서는 일과 시간이 아닌 때 호출해서 동작 안하는지 확인하고 일과 시간에 호출해서 동작하는지 확인해야 한다. 당연히 그렇게 테스트 스케줄을 짠다는 것 자체가 말이 안되는 이야기다. 

이런 경우 우리는 가짜 시간 객체를 시스템에게 던져주고 테스트를 할 수 있다.

time mocking

가짜 객체를 이용한 테스팅을 위해서는 vitest에서 제공하는 vi와 vi가 제공하는 utility 함수들을 사용한다. 실제 사용 예들을 보면서 어떻게 동작하는지 감을 익혀보자.

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'

// 매 테스트 전에 vitest에게 가짜 시간을 쓸 것이라고 알려줌
beforeEach(() => vi.useFakeTimers())

// 매 테스트 후에 원래 시간으로 돌려줌
afterEach(() => vi.useRealTimers())

test('업무시간 내에 요청했을 떄', () => {
  vi.setSystemTime(new Date(2000, 1, 1, 14)) // 필요한 가짜 시간으로 설정
  expect(purchase()).toBe(true)
})

test('업무시간 외에 요청했을 떄', () => {
  vi.setSystemTime(new Date(2000, 1, 1, 8))
  expect(purchase()).toBe(false)
})

먼저 매번 테스트가 실행되기 전에 호출되는 beforeEach에서는 가짜 시간 객체를 사용할 것임을 선언하고 있다.

그리고 각각의 테스트에서는 setSystemTime에 필요한 가짜 시간을 공급해서 원하는 환경을 만든 후 테스트를 진행한다.

마지막으로 afterEach에서는 가짜 객체를 쓰기로 했던 상황을 초기화 시켜준다.

 

그 외에도

위와 같은 경우 외에도 테스트하려는 코드가 의존하는 부분을 가짜 객체로 대체하기도 한다. 예를 들어 ajax 요청 후 결과를 활용하는 부분을 테스트 하려고 할 때 테스트 하련느 것은 요청과 응답의 활용인데 이를 위해서는 서버가 개입되어야 한다. 이때 mock server를 구성할 수 있다. 

백엔드 작업을 생각해보면 service는 dao를 사용하는데 service를 만들고 테스트 한다고 가정해보자. 이 경우도 dao는 service 입장에서 테스트 대상이 아니다. 이때 가짜 dao를 만들어 놓고 service만 테스트 할 수도 있을 것이다.

 

spy와 mock

 

spy vs mock

일반적으로 가짜 객체는 spy와 mock 두 가지로 구분된다.

구분 Spy Mock
용도
구현 되어있는 객체/함수에 대한 wrapping
주로 함수의 동작을 감시(호출이 되는지, 파라미터가 전달 되는지)가 목적
아직 구현되지 않는 객체/함수에 대한 가짜 객체
연관관계 함수와 무관하게 단위 테스트 진행 가능
Body
구현 필요성
이미 구현되어 있으므로 body 구현 불필요(가능은 함)
아직 구현이 없으므로 body 구현 필요
작성
방법
<T, K extends keyof T>
vi.spyOn(object: T,

                 method: K,
                 accessType?: 'get' | 'set') => MockInstance
vi.fn(fn?: Function) => Mock
호출
Spy를 직접 호출하지 않음
Mock 함수를 직접 호출
matchers
 - toHaveBeenCalled()                              // 단순히 호출 되었다.
 - toHaveBeenCalledTimes(n);                 // n번 호출 되었다.
 - toHaveBeenCalledWith(3);                   // 파라미터로 3이 전달 되었다.
 - toHaveReturned();                                 // 정상적으로 return 되었다.
 - toHaveReturnedWith('l')                        // l 값이 return 되었다.
함수 조작
(mockXX)
 - mockResultValue(value);                        // 호출 결과 반환 값을 value로 조작한다.
 - mockImplementation(function);            // 함수의 구현부를 function으로 대체한다.
 - mockImplementationOnce(function)    // 함수의 구현부를 function으로 1번만 대체한다.

spy나 mock은 처음에 작성하는 방법만 다를 뿐 결국에는 하나의 코드를 사용한다. 따라서 matchers나 함수 조작과 관련된 기능은 서로 동일하다.

matchers와 mockXX 함수는 매우 다양한데 이름만 봐도 어떤 용도인지 쉽게 알 수 있고 이름이 비슷한 고만 고만한 메서드들이 많아서 세부적으로 정리하는 것은 생략한다. 관심있는 경우 다음의 링크를 참조해서 살펴보자.

https://vitest.dev/api/expect.html#tohavebeencalled

 

Vitest

Next generation testing framework powered by Vite

vitest.dev

 

https://vitest.dev/api/mock.html

 

Vitest

Next generation testing framework powered by Vite

vitest.dev

 

spy 활용

먼저 spy를 활용하는 예를 살펴보자. 여기서는 String이 가지는 charAt 함수에 spy를 추가해서 모니터링 및 조작하는 예이다.

describe('함수에 대한 mocking - spy', () => {
  // 모든 테스트 마다 관련 mock 초기화
  afterEach(() => vi.restoreAllMocks())
  test('spy로 getLatest 확인하기', () => {
    const str = new String('Hello Spy')
    const spy = vi.spyOn(str, 'charAt')      // spyOn은 객체형만을 대상으로 한다.
    expect(str.charAt(3)).toBe('l')          // 일반적인 함수 사용

    // spy 활용 1: 함수의 호출 검사
    expect(spy).toHaveBeenCalled()           // 단순히 호출 되었다.
    expect(spy).toHaveBeenCalledTimes(1);    // 1번 호출 되었다.
    expect(spy).toHaveBeenCalledWith(3);     // 파라미터로 3이 전달 되었다.
    expect(spy).toHaveReturned();            // 정상적으로 return 되었다.
    expect(spy).toHaveReturnedWith('l')      // l 값이 return 되었다.

    // spy 활용 2: 함수의 조작: mockXXX
    spy.mockReturnValue('?')
    expect(str.charAt(3)).toBe('?')
    spy.mockImplementation(() => '이거 반환해')
    expect(str.charAt(3)).toBe('이거 반환해')
    expect(spy).toHaveBeenCalled(3)
  })
})

 

mock 활용

이번에는 mock의 활용 예를 살펴보자. calculator라는 객체는 multi, plus, getMultiAndPlus 3개의 함수를 갖는데 getMultiAndPlus는 multi와 plus를 사용한다. 이 함수를 테스트 하려는데 multi가 아직 미 구현인 상태이다. 이 상황을 mock으로 헤쳐나가보자.

describe('함수에 대한 mocking - mock', () => {
  const calculator = {
    multi:undefined,
    plus:(a, b)=>a+b,
    getMultiAndPlus:(a, b) => `${calculator.multi(a, b)}, ${calculator.plus(a, b)}`
  }
  test('mock으로 새로운 가짜 함수 만들고 사용하기', () => {
    //const mockMulti = vi.fn((a, b)=> a*b); // 생성 하면서 함수를 전달하거나
    const mockMulti = vi.fn().mockImplementation((a, b)=> a*b)  // 별도의 함수로 전달
    
    // mock으로 기존 함수 대체
    calculator.multi = mockMulti;

    calculator.getMultiAndPlus(1, 2);
    expect(mockMulti).toHaveBeenCalled()
    expect(mockMulti).toHaveBeenCalledTimes(1)
    expect(mockMulti).toHaveBeenCalledWith(1,2)
    expect(mockMulti).toHaveReturned()
    expect(mockMulti).toHaveReturnedWith(2)

    mockMulti.mockImplementationOnce(() => 'Mock! 이거 반환해')
    expect(calculator.getMultiAndPlus(1, 2)).toEqual('Mock! 이거 반환해, 3')
    expect(calculator.getMultiAndPlus(1, 2)).toBe("2, 3")
    expect(mockMulti).toHaveBeenCalledTimes(3)
  })
})

만약 vi.fn으로 mock 함수를 만들면서 아무런 구현부를 전달하지 않으면 ()=>{} 가 기본으로 사용된다.

 

Rest API mocking과 msw(Mock Service Worker)

 

Rest API 테스팅

이번에는 rest api를 테스팅 하는 내용에 대해 고민해보자. 다음의 함수가 잘 동작하는지 테스트 해야 한다고 생각해보자.

const fetchdata = async ()=>{
  const response = await fetch('http://myserver/path/to/posts');
  const json = await response.json();
  return json;
}

우리가 궁금한 것은 fetch 요청을 서버에 날리고 결과로 json이 잘 받아지는가 테스트 하는 것이다. 즉 서버가 어떻게 동작하는지가 아니라 "내가 요청을 잘 날렸나?" 와 "결과로 원하는 값을 받아서 사용하는가?"를 테스트 해야 한다.

서버가 오류가 있거나 다운되거나 또는 잘못 구현된 것은 테스트 관심사가 아니다. 따라서 이 부분이 배제된 상태에서 테스트가 진행되어야 한다. 가짜 서버가 필요해지는 시점이다.

 

요청과 그에 따른 응답 세트 생성

서버는 단순히 mock으로 만들기가 쉽지 않다. http 요청으로 호출되고, 응답에 상태도 있어야 하고 결과도 넣어줘야 하는 등 매우 복잡한 과정이 필요하다. 즉 fetch 함수만 mock으로 만들어서는 테스트 할수가 없다.(global 객체의 함수인 fetch는 spy가 않되는 것 같다. 혹시 방법을 아시는분~~)

따라서 이런일을 전문적으로 하는 Mock Service Worker 라는 모듈을 사용한다.

npm i -D msw

이 msw 모듈에는 rest라는 함수가 제공되는데 이를 통해서 처리할 요청과 생성할 응답을 만들 수 있다.

import { rest } from 'msw'

const restHandlers = [
  rest.get('http://myserver/path/to/posts', (req, res, ctx) => {
    return res(ctx.status(200), ctx.json(mockPostDatas))
  }),
]

const mockPostDatas = []
for (let i = 1; i < 10; i++) {
  mockPostDatas.push({ userId: i, id: i, title: `${i}th post title`, body: `${i}th post body` })
}

 

테스트 서버 생성 및 테스트

이제 설정된 rest들을 서비스 하도록 mock server를 설정하고 테스트해보자.

import { setupServer } from 'msw/node'
describe('mock service worker를 이용한 network 호출 테스트', () => {
  const server = setupServer(...restHandlers)

  // 서버를 시작한다. 만약 적합한 resthandler가 없을 경우 오류를 발생시킨다.
  beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))

  // 모든 테스트 종료 후 서버를 종료한다.
  afterAll(() => server.close())

  // 테스트 마다의 독립성을 유지하기 위해 server를 reset 해준다.
  afterEach(() => server.resetHandlers())

  test("기본 호출 확인", async ()=>{
    const data = await fetchdata();
    expect(data.length).toBe(9)
  });
})

 

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.