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 }

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

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

가짜 객체를 이용한 테스팅을 위해서는 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 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를 활용하는 예를 살펴보자. 여기서는 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의 활용 예를 살펴보자. 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를 테스팅 하는 내용에 대해 고민해보자. 다음의 함수가 잘 동작하는지 테스트 해야 한다고 생각해보자.

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)   }); })

 

 

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

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