51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Mock Service Worker:用于 Vue.js 开发和测试的 API Mocking

Vue.js 开发的一个不可避免的部分是我们需要一个 API 服务器来确保我们的前端代码正常工作。但是如果 API 服务器还没有准备好呢?我们的前端代码应该在哪里

MSW.001.opt.jpg

发送他们的 API 请求?如果前端团队必须等到后端团队完成他们的 API 服务器,那么效率就不那么高了。

我们可以通过 API mocking 来解决这个问题。它基本上只是一个前端可以向其发出 HTTP 请求的"假"服务器。

市场上有多种 API 模拟工具。他们正在以不同的方式解决同一个问题。有些允许您设置实际的 Node.js 模拟服务器。有些将拦截获取请求并将它们重定向到存在于前端的处理程序函数。

Mock Service Worker (MSW) 是一个令人兴奋的 API 模拟工具,它使用 Service Worker 拦截您的 HTTP 请求。这将允许您发出可以使用 DevTools 检查的实际 HTTP 请求,因为 MSW 在服务工作者级别上工作。

MSW 也可以在您的测试代码中使用,这样您就不必为 HTTP 响应设置额外的测试模拟。

为了在整篇文章中演示这一点,我们将使用 MSW 作为"假"服务器创建一个 Vue.js 应用程序,并使用相同的"假"服务器进行测试。

什么是Service Worker?

既然 MSW 依赖于 Service Worker,那我们就来说说 Service Worker 是什么。Service Worker 是一个与常规应用程序代码一起在后台运行的小程序,它负责处理诸如推送通知和响应缓存之类的事情。它旨在为前端应用程序创建健康的离线体验。MSW 建立在 Service Worker 的缓存机制之上。

准备一个新应用

让我们使用 Vue CLI 创建一个新的 Vue 应用程序。

在控制台中,使用 vue create 命令:

vue create my-app

选择第三个选项:

code1.jpg然后使用空格键选择单元测试(然后按 Enter):

code2.jpg选择版本 3:

code3.jpg然后(不是那么重要):

code4.jpg然后(不是那么重要):

code5.jpg现在,确保为单元测试解决方案提示选择"Jest"(这非常重要):

code6.jpg然后(不是那么重要):

code7.jpg最后,类型N为不保存预设:

code8.jpg一旦项目被创建,打开App.vue里面的src文件夹,替换为以下代码默认代码:

/src/App.vue

<template>
  <p>{{ message }}</p>
</template>

<script>
import { fetchMessage } from '@/services/fetchers'

export default {
  data() {
    return {
      message: ''
    }
  },
  async created() {
    try {
      this.message = await fetchMessage()
    }
    catch(error){
      this.message = 'server error :('
    }
  }
}
</script>

这就是我们的组件代码。它基本上只是一个使用 fetch 函数从服务器获取数据并以 HTML 显示返回数据的组件。(如果服务器返回错误,它将显示一条错误消息。)

如您所见,它正在导入/services/fetchers.

因此,让我们创建一个服务文件夹内的src *,*,并创建一个fetchers.js内部文件服务**文件夹,这将用于主机我们取功能。

将以下获取函数放在fetchers.js 中:

/src/services/fetchers.js

import axios from 'axios'

export const fetchMessage = async function (){
  const response = await axios.get('/message')
  return response.data.message
}

由于 fetcher 正在使用axios,我们也必须安装它:

npm install axios

现在您可以运行该应用程序:

npm run serve

并在浏览器中查看。

code10.jpg

应用程序运行成功。它说"服务器错误"只是因为我们实际上没有服务器。

所以,让我们准备一个"服务器"。

设置MSW

首先,将 MSW 安装到您的项目中:

npm install msw

然后在src 中创建一个名为mocks的文件夹。这是我们将放置所有模拟 API 代码的地方。

在mock文件夹中,创建两个文件,handlers.js和browser.js。

  • handlers.js将成为我们指定模拟 API 行为的地方。

  • browser.js将成为我们使用handlers.js 中的代码初始化实际模拟 Service Worker 的地方。由于MSW既可以用于开发,也可以用于测试,所以两个环境的初始化应该分开,browsers.js是用于开发的。(后面到MSW的测试部分我们会讲到测试的初始化)

我们将把所有的"假"API 请求处理程序放在handlers.js 中:

/src/mocks/handlers.js

import { rest } from 'msw'

export default [
  rest.get('/message', (req, res, ctx) => {
    return res(
      ctx.json({
        message: 'it works :)'
      })
    )
  })
]

这就是我们在 MSW 中指定模拟实现的方式。如果您熟悉 Express.js,则此处理程序语法应该是不言自明的。我们正在从这个文件中导出一个数组。但是由于我们目前只有一个处理程序,因此它是一个由一个处理程序组成的数组。

处理程序基本上将 API 路径映射到函数。该函数将安排对请求的响应。对于上述处理程序,对/messageAPI 路径的请求将提供包含以下 JSON 数据的响应:

{"message":"it works :)"}

在browser.js 中,我们将导入并使用该handlers数组来创建服务工作者:

/src/mocks/browser.js

import { setupWorker } from 'msw'
import handlers from './handlers'

export const worker = setupWorker(...handlers)

worker是我们的"假服务器"。

我们仍然需要在main.js 中导入和初始化它:

/src/main.js

import { createApp } from 'vue'
import App from './App.vue'

// NEW
if (process.env.NODE_ENV === 'development') {
  const { worker } = require('./mocks/browser')
  worker.start()
}

createApp(App).mount('#app')

worker是我们的"假服务器"。

我们仍然需要在main.js 中导入和初始化它:

/src/main.js

import { createApp } from 'vue'
import App from './App.vue'

// NEW
if (process.env.NODE_ENV === 'development') {
  const { worker } = require('./mocks/browser')
  worker.start()
}

createApp(App).mount('#app')

我们有条件地这样做是因为我们只需要在开发中运行它,而不是在生产中运行。

最后,我们将在 public 文件夹中生成一个 Service Worker 脚本:

npx msw init public/

这是通过拦截 HTTP 请求来实际引导我们的"假服务器"的脚本。(对于使用 MSW 的每个项目,这将是相同的脚本)

现在我们可以运行该应用程序,它会像真正存在的真实 API 服务器一样工作。

code11.jpg正如我所提到的,您可以打开 Chrome DevTools,选择 Network 选项卡,您可以看到发送到我们假 API 的实际请求:

code12.jpg

所以这就是MSW用于开发的方式,现在让我们谈谈测试。

用于测试的MSW

MSW 的美妙之处在于我们可以重用相同的handlers代码进行测试。

我们的测试将使用 Vue 测试库 (VTL) 而不是 Vue Test Utils。选择 VTL 的原因是因为它的设计理念可以更自然地与 MSW 配合使用。我们的测试将模拟用户实际使用应用程序的方式。例如,测试不会使用flushPromises。

我们必须为 VTL 安装一些东西。使用这两个库修改 package.json devDependencies:

"devDependencies": {
  "@testing-library/vue": "^6.3.4",
  "@testing-library/jest-dom": "^5.11.9",
  ...

(jest-dom 库将使我们能够在测试中使用更直观的断言方法。)

然后运行以下命令安装添加的库:

npm install

在tests/unit文件夹中,为我们的App.vue组件创建一个名为App.spec.js的测试文件。

另外,删除example.spec.js。那只是一个样本测试。

在App.spec.js 中,导入我们测试所需的所有内容:

/test/unit/App.spec.js

// our test subject
import App from '../../src/App'

// libraries
import { setupServer } from 'msw/node'
import { render, screen, waitFor } from '@testing-library/vue'
import '@testing-library/jest-dom'

// MSW handlers
import handlers from '../../src/mocks/handlers'

然后,使用以下命令创建"假服务器" setupServer

/test/unit/App.spec.js

const server = setupServer(...handlers)

请记住,我们setupWorker以前曾为开发创建了一个假服务器。现在我们使用不同的函数,setupServer因为测试将在 Node.js 环境中运行,而不是在实际的浏览器环境中。只有浏览器环境具有 Service Worker 功能,因此我们在测试中使用 MSW 的方式实际上并不涉及 Service Worker。

我们需要在任何测试之前启动服务器,并在完成测试后关闭它:

/test/unit/App.spec.js

const server = setupServer(...handlers)

// NEW
beforeAll(() => {
  server.listen()
})

// NEW
afterAll(() => {
  server.close()
})

现在,我们的第一个测试用例:

/test/unit/App.spec.js

describe('App', () => {
  it('calls fetchMessage once and displays a message', async () => {
    render(App)  
    await waitFor(() => {
      expect(screen.getByText('it works :)')).toBeInTheDocument()
    })
  })
})

基本上,它会renderApp组成部分,那么它会使用awaitwaitFor等待,直到文本"工程:)"出现在屏幕上。(大约一秒钟后,如果没有出现预期的文本,则测试将失败)

使用以下命令运行测试:

npm run test:unit

您应该会看到以下测试结果。

到目前为止,这是我们的测试代码:

/test/unit/App.spec.js

import App from '../../src/App'
import { setupServer } from 'msw/node'
import { render, screen, waitFor } from '@testing-library/vue'
import '@testing-library/jest-dom'
import handlers from '../../src/mocks/handlers'

const server = setupServer(...handlers)

beforeAll(() => {
  server.listen()
})

afterAll(() => {
  server.close()
})

describe('App', () => {
  it('calls fetchMessage once and displays a message', async () => {
    render(App)  
    await waitFor(() => {
      expect(screen.getByText('it works :)')).toBeInTheDocument()
    })
  })
})

函数调用

我们的测试确保组件正在显示来自我们 MSW"服务器"的数据,但是如果我们想确保它实际上正在调用axios以获取数据,我们必须使用称为"间谍"的东西。

间谍就像一个带有断言能力的模拟。但与常规模拟不同的是,间谍将调用实际实现------而不是模拟实现。我们将监视 fetcher 以确保它确实在我们的组件内部被调用。

首先,让我们使用星号语法导入 fetcher:

/test/unit/App.spec.js

...
import '@testing-library/jest-dom'
import handlers from '../../src/mocks/handlers'
import * as fetchers from '@/services/fetchers' // NEW

这会将所有内容fetchers作为单个对象导入。虽然我们只需要这个fetchMessage函数,但是我们创建一个被监视的函数的方式将要求该函数位于一个对象内部。

现在,我们可以使用jest.spyOn以下方法创建监视方法:

/test/unit/App.spec.js

...
import '@testing-library/jest-dom'
import handlers from '../../src/mocks/handlers'
import * as fetchers from '@/services/fetchers'

const fetchMessageSpy = jest.spyOn(fetchers, 'fetchMessage') // NEW

现在,无论何时fetchMessage在代码中的任何地方被调用,fetchMessageSpy都会记录下来。

所以我们可以断言它被调用了多少次:

/test/unit/App.spec.js

describe('App', () => {
  it('calls fetchMessage once and displays a message', async () => {
    render(App)  
    await waitFor(() => {
      expect(screen.getByText('it works :)')).toBeInTheDocument()
    })
    expect(fetchMessageSpy).toHaveBeenCalledTimes(1) // NEW
  })})

最后,确保在每个测试用例之后重置 spy:

/test/unit/App.spec.js

beforeAll(() => {
  server.listen()
})

// NEW
afterEach(() => {
  fetchMessageSpy.mockClear()
})

afterAll(() => {
  server.close()
})

再次运行测试npm run test:unit。

到目前为止,这是我们的测试代码:

/test/unit/App.spec.js

import App from '../../src/App'
import { setupServer } from 'msw/node'
import { render, screen, waitFor } from '@testing-library/vue'
import '@testing-library/jest-dom'
import handlers from '../../src/mocks/handlers'
import * as fetchers from '@/services/fetchers'

const server = setupServer(...handlers)

const fetchMessageSpy = jest.spyOn(fetchers, 'fetchMessage')

beforeAll(() => {
  server.listen()
})

afterEach(() => {
  fetchMessageSpy.mockClear()
})

afterAll(() => {
  server.close()
})

describe('App', () => {

  it('calls fetchMessage once and displays a message', async () => {
    render(App)  
    await waitFor(() => {
      expect(screen.getByText('it works :)')).toBeInTheDocument()
    })
    expect(fetchMessageSpy).toHaveBeenCalledTimes(1)
  })
})

服务器错误

我们只测试了该组件在成功提供请求时是否正常工作。但是,有时服务器无法正常服务;它会返回类似状态500错误的东西。我们必须编写一个测试用例来确保组件能够优雅地处理此类错误。

有了上面的设置,我们现在可以编写我们的第二个测试:

/test/unit/App.spec.js

describe('App', () => {
  it('calls fetchMessage once and displays a message', async () => {
    ...
  })

  // NEW
  it('shows an error message on server error', async () => {
    render(App)
    await waitFor(() => {
      expect(screen.getByText('server error :(')).toBeInTheDocument()
    })
    expect(fetchMessageSpy).toHaveBeenCalledTimes(1)
  })
})

除了测试描述和getByText 参数之外,这几乎与第一个测试相同。

我们当前的 MSW 处理程序只会返回"成功"响应。当我们必须测试组件的错误处理能力时,我们必须创建一个额外的"失败"处理程序并换出"成功"处理程序。

在测试开始时,我们将使用"失败"处理程序来替换我们当前的处理程序:

/test/unit/App.spec.js

it('shows an error message on server error', async () => {
    // NEW
    server.use(rest.get('/message', (req, res, ctx) => {
      return res(ctx.status(500))
    }))

    render(App)
    ...

这将返回状态500服务器错误。反过来,它会触发catch我们App组件中的子句。结果,将显示错误消息"服务器错误:("。

由于我们使用rest.get,我们不得不进口rest来自msw

/test/unit/App.spec.js

import App from '../../src/App'
import { setupServer } from 'msw/node'
import { rest } from 'msw' // NEW
...

最后,我们将确保在每次测试后将假服务器重置回原始处理程序:

/test/unit/App.spec.js

afterEach(() => {
  fetchMessageSpy.mockClear()
  server.resetHandlers() // NEW
})

再次运行测试npm run test:unit以确保它们都通过。

这是我们最终的测试代码:

/test/unit/App.spec.js

import App from '../../src/App'
import { setupServer } from 'msw/node'
import { rest } from 'msw'
import { render, screen, waitFor } from '@testing-library/vue'
import '@testing-library/jest-dom'
import handlers from '../../src/mocks/handlers'
import * as fetchers from '@/services/fetchers'

const server = setupServer(...handlers)

const fetchMessageSpy = jest.spyOn(fetchers, 'fetchMessage')

beforeAll(() => {
  server.listen()
})

afterEach(() => {
  fetchMessageSpy.mockClear()
  server.resetHandlers()
})

afterAll(() => {
  server.close()
})

describe('App', () => {

  it('calls fetchMessage once and displays a message', async () => {
    render(App)  
    await waitFor(() => {
      expect(screen.getByText('it works :)')).toBeInTheDocument()
    })
    expect(fetchMessageSpy).toHaveBeenCalledTimes(1)
  })

  it('shows an error message on server error', async () => {
    server.use(rest.get('/message', (req, res, ctx) => {
      return res(ctx.status(500))
    }))
    render(App)
    await waitFor(() => {
      expect(
        screen.getByText('server error :(')
      ).toBeInTheDocument()
    })
    expect(fetchMessageSpy).toHaveBeenCalledTimes(1)
  })
})

概括

如你看到的。MSW 不仅用于测试。即使您还没有进行自动化测试,您仍然可以使用这个很酷的库。如果您尝试进行自动化测试,MSW 将使过程更容易,因为模拟 API 已经到位。我们将在即将到来的真实世界测试课程中介绍更多真实世界的测试实践。

赞(0)
未经允许不得转载:工具盒子 » Mock Service Worker:用于 Vue.js 开发和测试的 API Mocking