前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编写接口请求库单元测试与 E2E 测试的思考

编写接口请求库单元测试与 E2E 测试的思考

作者头像
Innei
发布2021-12-28 10:37:02
1K0
发布2021-12-28 10:37:02
举报
文章被收录于专栏:静之森

最近在写适配 Mx Space Server 的 JS SDK。因为想写一个正式一点的库,以后真正能派的上用场的,所以写的时候尽量严谨一点。所以单测和 E2E 也是非常重要。

架构设计

先说说我这个接口库是怎么封装了,然后再说怎么去测试。首先我采用的是适配器模式,也就是不依赖任何一个第三方请求库,你可以用 axios、ky、umi-request、fetch 任何一个库,只需要编写一个符合接口标准的适配器。这里以 axios 为例。

配适器接口如下,目前比较简单。

tsx

代码语言:javascript
复制
1export interface IRequestAdapter<T = any> {
2  default: T
3  get<P = unknown>(
4    url: string,
5    options?: RequestOptions,
6  ): RequestResponseType<P>
7
8  post<P = unknown>(
9    url: string,
10    options?: RequestOptions,
11  ): RequestResponseType<P>
12
13  patch<P = unknown>(
14    url: string,
15    options?: RequestOptions,
16  ): RequestResponseType<P>
17
18  delete<P = unknown>(
19    url: string,
20    options?: RequestOptions,
21  ): RequestResponseType<P>
22
23  put<P = unknown>(
24    url: string,
25    options?: RequestOptions,
26  ): RequestResponseType<P>
27}

COPY

实现 axios-adaptor 如下:

tsx

代码语言:javascript
复制
1import axios, { AxiosInstance } from 'axios'
2import { IRequestAdapter } from '~/interfaces/instance'
3const $http = axios.create({})
4
5// ignore axios `method` declare not assignable to `Method`
6export const axiosAdaptor: IRequestAdapter<AxiosInstance> = {
7  get default() {
8    return $http
9  },
10
11  get(url, options) {
12    // @ts-ignore
13    return $http.get(url, options)
14  },
15  post(url, options) {
16    const { data, ...config } = options || {}
17    // @ts-ignore
18    return $http.post(url, data, config)
19  },
20  put(url, options) {
21    const { data, ...config } = options || {}
22    // @ts-ignore
23    return $http.put(url, data, config)
24  },
25  delete(url, options) {
26    const { ...config } = options || {}
27    // @ts-ignore
28    return $http.delete(url, config)
29  },
30  patch(url, options) {
31    const { data, ...config } = options || {}
32    // @ts-ignore
33    return $http.patch(url, data, config)
34  },
35}

COPY

然后在构造 client 的时候要注入 adaptor。如下:

tsx

代码语言:javascript
复制
1const client = createClient(axiosAdaptor)(endpoint)
2client.post.post.getList(page, 10, { year }).then((data) => {
3  // do anything
4})

COPY

注入 adaptor 后,所有请求方法将使用 adaptor 中的相关方法。

这样做的好处是比较灵活,适用各类库,体积也能做到比较小。类似的 NestJS 等框架也是用了适配器模式,所以 NestJS 可以灵活选择 Express、Koa、Fastify 等。

坏处就是需要编写适配器,对新手来说可能不太友好,但是可以提供默认适配器去缓解这个问题。其次是适配器中方法返回类型是一定的,如错误的使用 axios 的 interceptor 可能会导致出现问题。

Unit Test

再说说单测,一般接口库也主要做这类测试比较多,因为单测不需要实际去访问接口,都是用 mock 的方式去伪造一个数据,而用 Jest 的话就直接 spyOn 去 mock 掉整个请求方法了。

这里用 axios 为默认适配器,那么就是在测试中 mock 掉 axios 的请求方法(axios.get, axios.post, ...)因为 axios 的逻辑你是不需要关心也不需要测试的。你只需要测试自己的业务逻辑就行了。

而对于这个库而言只需要测试有没有注入 adaptor 后,用 adaptor 请求数据之后有没有拿到了正确的值。如图所示,只需要测试 core 的逻辑,也就是注入 adaptor 之后有没有正确使用 adaptor 去请求,以及用 adaptor 请求拿到数据之后有没有正确处理数据。而关于请求了啥数据,并不关心,所以直接 mock 掉 axios 这层。

flowchart TD id1([core: client]) --> id2([adaptor: axios]) -- use adaptor to fetch data --> id3([core: handle data]) --> id5([return data])

所以测试可以这样去写:

tsx

代码语言:javascript
复制
1 describe('client `get` method', () => {
2    afterEach(() => {
3      jest.resetAllMocks()
4    })
5    test('case 1', async () => {
6      jest.spyOn(axiosAdaptor, 'get').mockImplementation((url, config) => {
7        if (url === 'http://127.0.0.1:2323/a/a?foo=bar') {
8          return Promise.resolve({ data: { ok: 1 } })
9        }
10
11        return Promise.resolve({ data: null })
12      })
13
14      const client = generateClient()
15      const data = await client.proxy.a.a.get({ params: { foo: 'bar' } })
16
17      expect(data).toStrictEqual({ ok: 1 })
18    })
19
20    test('case 2', async () => {
21      jest.spyOn(axiosAdaptor, 'get').mockImplementation((url, config) => {
22        if (url === 'http://127.0.0.1:2323/a/a') {
23          return Promise.resolve({ data: { ok: 1 } })
24        }
25
26        return Promise.resolve({ data: null })
27      })
28
29      const client = generateClient()
30      const data = await client.proxy.a.a.get()
31
32      expect(data).toStrictEqual({ ok: 1 })
33
34      {
35        jest.spyOn(axiosAdaptor, 'get').mockImplementation((url, config) => {
36          if (url === 'http://127.0.0.1:2323/a/b') {
37            return Promise.resolve({ data: { ok: 1 } })
38          }
39
40          return Promise.resolve({ data: null })
41        })
42
43        const client = generateClient()
44        const data = await client.proxy.a.b.get()
45
46        expect(data).toStrictEqual({ ok: 1 })
47      }
48    })
49  })

COPY

如上,直接用 Jest spyOn 掉了 adaptor 的 get 方法,而要测试的则是 core 层有没有正确使用 adaptor 访问了正确的路径。所以在 mockImplementation 中,判断了是不是这个这个 url。

以上则是单测中的一环,client - adaptor - core 的测试。

然后说说单个接口怎么去写单测。我感觉这里其实没有什么必要去写。但是写了还是写一下,我也不知道有什么好的办法。还是使用 mock 的方法 mock 掉 adaptor 的请求返回。简单说说就是这样写了。

比如测试接口 /comments/:id:

ts

代码语言:javascript
复制
1describe('test note client', () => {
2  const client = mockRequestInstance(CommentController)
3
4  test('get comment by id', async () => {
5    const mocked = mockResponse('/comments/11111', {
6      ref_type: 'Page',
7      state: 1,
8      children: [],
9      comments_index: 1,
10      id: '6188b80b6290547080c9e1f3',
11      author: 'yss',
12      text: '做的框架模板不错. (•౪• ) ',
13      url: 'https://gitee.com/kmyss/',
14      key: '#26',
15      ref: '5e0318319332d06503619337',
16      created: '2021-11-08T05:39:23.010Z',
17      avatar:
18        'https://sdn.geekzu.org/avatar/8675fa376c044b0d93a23374549c4248?d=retro',
19    })
20
21    const data = await client.comment.getById('11111')
22    expect(data).toEqual(mocked)
23  })
24}

COPY

这边主要就是测试这个方法中请求的路径有没有写对了,但是非常关键的是用例中的路径一定要写对,上面那个的话就是 /comments/11111mockResponse是我封装的一个测试方法。具体参考:

@mx-space/api-client:__test__/helper

E2E test

E2E 是点对点测试,是需要去真实访问接口的,这也是最接近用户实际开发体验的测试,也就是说不 mock 掉 adaptor,也不在业务层用假数据的。当然假数据还是要用的,只是需要起一个额外的服务器去挂数据,以便真实去请求数据。

E2E 就是去测试 adaptor 了,因为上面单测除了 adaptor 没测。

我已 Express 、 Jest 为例。我的想法是直接用 Express 托管一系列接口。当然不是手动去启动一个服务,而是 Express 直接跑在 Jest 测试中。

首先写一个方法,起一个 Express 实例。

ts

代码语言:javascript
复制
1// __tests__/helpers/e2e-mock-server.ts
2import cors from 'cors'
3import express from 'express'
4import { AddressInfo } from 'net'
5type Express = ReturnType<typeof express>
6export const createMockServer = (options: { port?: number } = {}) => {
7  const { port = 0 } = options
8
9  const app: Express = express()
10  app.use(express.json())
11  app.use(cors())
12  const server = app.listen(port)
13
14  return {
15    app,
16    port: (server.address() as AddressInfo).port,
17    server,
18    close() {
19      server.close()
20    },
21  }
22}

COPY

port 建议为 0,0 表示使用随机一个空闲的端口。因为固定端口在 Jest 并行测试中容易被占用。

测试用例也比较好写,只要按照传统前后端接口请求去写就可以了。如下:

ts

代码语言:javascript
复制
1import { allControllers, createClient, HTTPClient, RequestError } from '~/core'
2import { IRequestAdapter } from '~/interfaces/instance'
3import { createMockServer } from './e2e-mock-server'
4
5export const testAdaptor = (adaptor: IRequestAdapter) => {
6  let client: HTTPClient
7  const { app, close, port } = createMockServer()
8
9  afterAll(() => {
10    close()
11  })
12  beforeAll(() => {
13    client = createClient(adaptor)('http://localhost:' + port)
14    client.injectControllers(allControllers)
15  })
16  test('get', async () => {
17    app.get('/posts/1', (req, res) => {
18      res.send({
19        id: '1',
20      })
21    })
22    const res = await client.post.getPost('1')
23
24    expect(res).toStrictEqual({
25      id: '1',
26    })
27  })
28
29  test('post', async () => {
30    app.post('/comments/1', (req, res) => {
31      const { body } = req
32
33      res.send({
34        ...body,
35      })
36    })
37    const dto = {
38      text: 'hello',
39      author: 'test',
40      mail: '1@ee.com',
41    }
42    const res = await client.comment.comment('1', dto)
43
44    expect(res).toStrictEqual(dto)
45  })
46
47  test('get with search query', async () => {
48    app.get('/search/post', (req, res) => {
49      if (req.query.keyword) {
50        return res.send({ result: 1 })
51      }
52      res.send(null)
53    })
54
55    const res = await client.search.search('post', 'keyword')
56    expect(res).toStrictEqual({ result: 1 })
57  })
58
59  test('rawResponse rawRequest should defined', async () => {
60    app.get('/search/post', (req, res) => {
61      if (req.query.keyword) {
62        return res.send({ result: 1 })
63      }
64      res.send(null)
65    })
66
67    const res = await client.search.search('post', 'keyword')
68    expect(res.$raw).toBeDefined()
69    expect(res.$raw.data).toBeDefined()
70  })
71
72  it('should error catch', async () => {
73    app.get('/error', (req, res) => {
74      res.status(500).send({
75        message: 'error message',
76      })
77    })
78    await expect(client.proxy.error.get()).rejects.toThrowError(RequestError)
79  })
80}
81
82// __test__/adaptors/axios.ts
83
84import { umiAdaptor } from '~/adaptors/umi-request'
85import { testAdaptor } from '../helpers/adaptor-test'
86describe('test umi-request adaptor', () => {
87  testAdaptor(umiAdaptor)
88})

COPY

上面封装了一个方法去测试 adaptor,有多次 adaptor 的话比较方便。

测试主要覆盖了,adaptor 接口是否正确,请求构造是否正确,返回数据是否正确。

写起来还是比较简单的,注意的是,测试跑完后不要忘了把 Express 销毁,即 server.close()

完整项目参考:

mx-space/api-client

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-12-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 架构设计
  • Unit Test
  • E2E test
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档