Mock Service Worker에서 jest를 누릅니다.mock을 사용하지 않고 비동기 요청 테스트를 쓰기

Mock Service Worker에 대해서 많이 해봤으니까 소개해 드릴게요.

Mock Service Worker란 무엇입니까?


Mock Service Worker(이하 msw)는 네트워크 수준에서 API 요청을 차단하고 mock 데이터를 반환하는 데 사용되는 라이브러리입니다.API 요청이 포함된 처리 테스트, SPA 개발 시 mock 서버로 사용할 수 있습니다.
https://mswjs.io/
다음 테스트에 사용된 샘플 코드입니다.setupServer에서 차단에 사용할 서버를 정의하고 listen()에서 차단을 시작하고 close()에서 차단을 중지합니다.
함수명setupServer이지만 실제로 서버가 만들어진 것은 아니고 내부는 노드다.js의 API에 로컬 모듈 httpsXMLHttpRequest 패치를 요청하여 동작을 진행합니다.
https://mswjs.io/docs/api/setup-server#operation
import { rest } from "msw"
import { setupServer } from "msw/node";
import axios from "axios";


// mockサーバー
const mockServer = setupServer(
  rest.get('/greeting', (req, res, ctx) => {
    return res(ctx.status(200), ctx.json('Hello'))
  })
)

// テスト対象の関数
const greeting = async (name: string) => {
  const word =  await axios.get('/greeting')
  return `${word.data} ${name}`
}

describe('greeting', () => {
  beforeAll(() => {
    // インターセプトスタート
    mockServer.listen()
  })
  afterAll(() => {
    // インターセプトストップ
    mockServer.close()
  })

  test('挨拶を返す', async () => {
    const result = await greeting('ryo')

    expect(result).toEqual('Hello ryo') // Green
  })
})

왜 네트워크 수준의 모크가 필요합니까?


jest.mock 네트워크 등급의 모듈이 필요하지 않습니까? 왜냐하면 모크의 층이 낮을수록 테스트가 안전하기 때문입니다.다음 장에서 상세한 예를 쓰겠지만 시뮬레이터라면 그 모듈 자체의 오류는 그 테스트에서 보장할 수 없다.
일반적으로 더 안전한 테스트를 쓰기 위해서는 모크를 사용하지 않거나 도층을 낮추는 것이 필요하다.
※ 모크를 사용해 테스트 수행의 성능을 높일 수 있는 장점이 있어 사례입니다.

Vue 구성 요소 테스트에서의 사용 예


나는 실제 용례에 존재할 수 있는 로그인 처리에 대한 테스트 코드를 쓸 것이다.
테스트 프레임워크는 Jest 및 Vue Testing Library를 사용합니다.
https://github.com/facebook/jest
https://github.com/testing-library/vue-testing-library

테스트 대상 코드


다음은 로그인 폼의 구성 요소입니다.
이 구성 요소의 기능은 다음과 같다.
  • 사용자 이름과 비밀번호를 입력할 수 있음
  • submit 버튼을 눌러 로그인 API 호출
  • 로그인에 성공하면 Hello <username>
  • 표시
  • 로그인 실패 시 Error
  • 표시
    Login.vue
    <template>
      <h1 v-if="user">
        Hello, {{ user.name }}
      </h1>
      <form @submit.prevent="handleAuth">
        <label for="username">Username:
          <input v-model="formData.username" id="username" name="username"/>
        </label>
        <label for="password">Password:
          <input v-model="formData.password" id="password" name="password"/>
        </label>
        <button>submit</button>
      </form>
      <span v-if="error" data-testid="error">{{ error }}</span>
    </template>
    
    <script lang="ts">
    import { defineComponent, reactive, ref } from "@vue/runtime-core";
    import axios from 'axios';
    
    type User = {
      name: string,
    }
    
    export default defineComponent({
      setup() {
        const formData = reactive({
          username: '',
          password: '',
        })
        const user = ref<null | User>(null)
        const error = ref<null | string>(null)
    
        const handleAuth = async () => {
          try {
            const response = await axios.post('/login', {
              username: formData.username,
              password: formData.password
            })
            user.value = response.data
          } catch (e) {
            error.value = e.response.data.error
          }
        }
        return {
          formData,
          handleAuth,
          user,
          error
        };
      }
    });
    </script>
    

    Jest.모크를 사용하여 axios를 모듈화한 코드


    우선, 첫 번째.ack을 사용하여 axios의 예를 표시합니다.jest.mock('axios')에서 axios의 모듈을 Mock하고 mockResolvedValue, mockRejectedValue에서 Mock의 반환 값을 정의하여 정상적인 시스템과 비정상적인 시스템의 UI를 테스트한다.
    Login.vue.test
    import { render, screen, fireEvent } from '@testing-library/vue'
    import Login from "../../components/Login.vue"
    import axios from 'axios'
    
    jest.mock('axios')
    const mockedAxios = axios as jest.Mocked<typeof axios>
    
    describe('Login', () => {
      test('ログインが成功した場合にユーザー名を表示する', async () => {
        mockedAxios.post.mockResolvedValue(  { data: { name: 'validUsername'}})
    
        render(Login)
    
        expect(screen.queryByText('Hello, validUsername')).toBeFalsy()
    
        await fireEvent.update(screen.getByLabelText(/username/i), 'validUser')
        await fireEvent.update(screen.getByLabelText(/password/i), 'validPassword')
        await fireEvent.click(screen.getByText('submit'))
    
        expect(await screen.findByText('Hello, validUsername')).toBeTruthy()
        expect(screen.queryByTestId('error')).toBeFalsy()
      })
    
      test('ログインが失敗した場合にエラーを表示する', async () => {
        mockedAxios.post.mockRejectedValue( { response: { data: { error: 'error: invalid username or password'}} })
    
        render(Login)
    
        expect(screen.queryByText('Hello, validUsername')).toBeFalsy()
    
        await fireEvent.update(screen.getByLabelText(/username/i), 'invalidUser')
        await fireEvent.update(screen.getByLabelText(/password/i), 'invalidPassword')
        await fireEvent.click(screen.getByText('submit'))
    
        expect(await screen.findByTestId('error')).toBeTruthy()
        expect(screen.queryByText('Hello')).toBeFalsy()
      })
    })
    
    언뜻 보기에는 문제가 없지만 이 테스트는 직접mock axios의 것이기 때문에 axios의 모듈 자체에 버그가 있고 기능이 없는 경우나 BREAKING CHANGE의 axios의 반환값이 변화하는 경우(예를 들어 data의 랩이 없는 경우)도 테스트를 통과할 수 있다.

    msw를 사용하여 네트워크 단계로 모듈화된 코드


    다음은 msw를 사용하는 예입니다.setupServer에서 /login의 POST 요청을 차단하는 정의를 내렸다.내부에서 요구된usename과password를 비교하여 정상 시스템과 이상 시스템의 반응을 분리한다.
    테스트 코드는 매우 간단하다.단지 표에 값을 넣고 발송합니다.
    Login.test.ts
    import { rest } from "msw"
    import { render, screen, fireEvent } from '@testing-library/vue'
    import { setupServer } from "msw/node";
    import Login from "../../components/Login.vue"
    
    
    const VALID_USER = {
      username: 'validUsername',
      password: 'validPassword'
    }
    
    const mockServer = setupServer(
      rest.post<Record<string, any>>('/login', (req, res, ctx) => {
        const { username, password } = req.body
    
        if (username !== VALID_USER.username && password !== VALID_USER.password) {
          return res(
            ctx.status(403),
            ctx.json({
              error: 'error: invalid username or password'
            })
          )
        }
        return res(
          ctx.status(200),
          ctx.json({
            name: 'validUsername',
          })
        )
      })
    )
    
    describe('Login', () => {
      beforeAll(() => mockServer.listen())
      afterEach(() => mockServer.resetHandlers())
      afterAll(() => mockServer.close())
    
      test('ログインが成功した場合にユーザー名を表示する', async () => {
        render(Login)
    
        expect(screen.queryByText('Hello, validUsername')).toBeFalsy()
    
        await fireEvent.update(screen.getByLabelText(/username/i), VALID_USER.username)
        await fireEvent.update(screen.getByLabelText(/password/i), VALID_USER.password)
        await fireEvent.click(screen.getByText('submit'))
    
        expect(await screen.findByText('Hello, validUsername')).toBeTruthy()
        expect(screen.queryByTestId('error')).toBeFalsy()
      })
    
      test('ログインが失敗した場合にエラーを表示する', async () => {
        render(Login)
    
        expect(screen.queryByText('Hello, validUsername')).toBeFalsy()
    
        await fireEvent.update(screen.getByLabelText(/username/i), 'invalid user')
        await fireEvent.update(screen.getByLabelText(/password/i), 'invalid password')
        await fireEvent.click(screen.getByText('submit'))
    
        expect(await screen.findByTestId('error')).toBeTruthy()
        expect(screen.queryByText('Hello')).toBeFalsy()
      })
    })
    
    이렇게 되면 사용jest.mock과 달리 axios의 모듈 자체 오류, BREAKING CHANGE의 반환값이 변경된 경우 테스트에 실패합니다.더 안전한 테스트야.

    노드와의 비교


    msw와 같은 네트워크급 차단 라이브러리nock로도 유명하다.
    https://github.com/nock/nock
    msw 문서에 nock과 비교가 있기 때문에 기재해야 합니다.
    표준
    Nock
    Mock Service Worker
    지원되는 API
    REST
    REST/GraphQL
    컨디션
    Node
    Node/Browser
    이루어지다
    Monky 패치를 http/http/XMLHttpRequest 모듈에 부착
    Monky 패치를 http/http/XMLHttpRequest 모듈에 부착
    적분
    기존 코드를 변경할 필요가 없습니다.axios 등 발행 라이브러리에 대응하는 어댑터가 필요합니다.
    기존 코드를 변경할 필요가 없습니다.어댑터가 필요하지 않습니다.
    정의
    방법을 통해 정의 모듈 검사
    함수 정의 모듈
    Nock에 비해 GraphiQL에 대응하는 점, Browser도 사용할 수 있는 점, 어댑터에 필요 없는 점이 좋다.
    여기도 다른 도서관과 비교가 된다.
    https://mswjs.io/docs/comparison

    끝맺다


    지금까지 "Mock Service Worker에서 Jest.mock을 사용하지 않고 비동기 요청을 쓰는 테스트"였습니다.사용jest.mock의 경우보다 기술량이 증가하지만 테스트에서의 안전성을 확보할 수 있기 때문에 더욱 좋다.또 패스 오류 등으로 쉽게 끌리거나 곤란해하지 않는다.앞으로도 업무적으로 활용하고 싶어요.

    좋은 웹페이지 즐겨찾기