阅读之前
希望你能有以下基础, 方便阅读:
ECMAScript 6 (ES6)
为什么需要 Mock
这样的场景, 相信大家会觉得似曾相识.
现今的业务系统已经很少是孤立存在的了, 尤其对于一个大公司而言, 各个部门之间的配合非常密切, 我们或多或少都需要使用兄弟团队或是其他公司提供的接口服务. 这样的话, 就对我们的联调和测试造成了很大的麻烦. 假如各个兄弟部门的步伐完全一致, 那么问题就会少很多, 但理想很丰满, 现实却很骨感, 要做到步伐一致基本是不可能的.
为此, 我们就需要使用一些工具来帮助我们将业务单元之间尽量解耦, 它就是 Mock
什么是 Mock
如果将 mock 单独翻译过来, 其意义为 "虚假, 虚设", 因此在软件开发领域, 我们也可以将其理解成 "虚假数据", 或者 "真实数据的替身".
Mock 的好处
团队可以更好地并行工作
当使用 mock 之后, 各团队之间可以不需要再互相等待对方的进度, 只需要约定好相互之间的数据规范(文档), 即可使用 mock 构建一个可用的接口, 然后尽快的进行开发和调试以及自测, 提升开发进度的的同时, 也将发现缺陷的时间点大大提前.
开启 TDD(Test-Driven Development)模式, 即测试驱动开发
单元测试是 TDD 实现的基石, 而 TDD 经常会碰到协同模块尚未开发完成的情况, 但是有了 mock, 这些一切都不是问题. 当接口定义好后, 测试人员就可以创建一个 Mock, 把接口添加到自动化测试环境, 提前创建测试.
测试覆盖率
比如一个接口在各种不同的状态下要返回不同的值, 之前我们的做法是复现这种状态然后再去请求接口, 这是非常不科学的做法, 而且这种复现方法很大可能性因为操作的时机或者操作方式不当导致失败, 甚至污染之前数据库中的数据. 如果我们使用 mock, 就完全不用担心这些问题.
方便演示
通过使用 Mock 模拟数据接口, 我们即可在只开发了 UI 的情况下, 无须服务端的开发就可以进行产品的演示.
隔离系统
在使用某些接口的时候, 为了避免系统中数据库被污染, 我们可以将这些接口调整为 Mock 的模式, 以此保证数据库的干净.
在吹了这么多的 Mock 之后, 相信大家一定跃跃欲试了, 那么接下来我们谈一谈实现 Mock 的几种方法.
实现 Mock
"倔强青铜"
好了, 我们先从最倔强的 "青铜" 开始吧, 在没有 mock 的时候, 我们是如何在没有真实接口的情况下进行开发的呢?
在本人的记忆里, 当遇到这种情况, 我最开始的做法就是将数据先写死在业务中, 比如:
- // API
- import API from '../api/index';
- function getApiMessage() {
- return new Promise((resolve) => {
- resolve({
- message: '请求成功'
- });
- })
- // return API.getApiMessage();
- }
我会将真实的请求注释掉, return 一个 resolve 假数据的 promise 代替真实的请求, 然后我在调用这个方法的时候就会返回一个 resolve 我自己定义的虚假数据的 promise 而不是从尚未完成的接口获得的 promise. 看起来还不错, 起码我能够在没有接口的情况下继续进行开发了. 虽然当遇到复杂的列表数据的时候, 自己写起来有点手疼.
但是虚假数据和业务如此耦合真的好吗? 假如当真正的接口完成之后, 因为业务可以 "正确运行" 而忘记了移除这些虚假数据, 导致实际你使用的数据一直是你自己编造而非真实的, 那可是相当严重的问题. 所以我们接下来需要思考的便是如何尽量的减少在业务代码中写入这些虚假数据. 为了达成这个目标, 让我们正式晋级 mock 的 "荣耀黄金" 段位.
"荣耀黄金"
在 mock 的 "荣耀黄金" 段位, 我们拥有了一个非常好用的工具: mockJs http://mockjs.com/ , 通过使用 mockJs 我们能根据模板和规则生成复杂的接口数据, 而无需我们自己动手去书写, 例如:
- // API
- import API from '../api/index';
- import Mock from 'mockjs';
- function getApiMessage() {
- return new Promise((resolve) => {
- resolve(Mock.mock({
- list|1-20: ['mock 数据']
- });
- })
- // return API.getApiMessage();
- }
- /**
- * 通过 Mock.mock 方法和 list|1-20: ['mock 数据'] 模板
- * 我们将生成一个长度为 1-20, 每个值都为'mock 数据' 数组
- */
但是这样做始终只不过是方便了我们 "造假" 而已, 并不能将 "假货" 真的从我们的业务代码中移除出去. 为了实现这个目的, 我们不妨先来分析我们的需求:
模拟数据与业务代码完全分离
通过一些配置, 达到只 mock 部分数据, 大部分的数据还是从请求中获取
首先, 如果我们要想要模拟数据和业务代码完全分离, 我们必须要想办法在请求的时候做一些文章, 让其在请求的时候去获取 mock 数据而非去请求真正的接口, 也就是所谓的 "请求拦截", 而实现请求拦截也同样有两种方式:
修改请求链接到 mock-server, 在 mock-server 配置 mock 数据和路由
- // API/index.JS
- // 通过新增 getDataUseMock 方法来说明使用了 mock 方法
- import request from '../request';
- function getDataUseMock(data) {
- request({
- mock: true
- });
- }
- // request/index.JS
- const mockServer = 'http://127.0.0.1:8081';
- function request(opt) {
- if (opt.mock) {
- const apiName = opt.API;
- opt.url = `${mockServer}/${apiName}`;
- }
- ...
- }
直接在检测使用 mock 时, 从 mock 数据文件中取出对应 key 值的数据
- // API/index.JS
- // 通过新增 getDataUseMock 方法来说明使用了 mock 方法
- import request from '../request';
- function getDataUseMock(data) {
- request({
- mock: true
- });
- }
- // request/index.JS
- import mockData from 'mock/db.js';
- function request(opt) {
- if (opt.mock) {
- const apiName = opt.API;
- return new Promise((resolve) => {
- resolve(mockData.apiName)
- })
- }
- ...
- }
- //mock/db.JS
- export default {
- '/api/test': {
- msg: '请求成功'
- }
- }
乍一看好像第二种方式似乎更简单, 事实也确实如此, 但是考虑到如果我是直接从文件中直接读取数据, 那么业务上的行为也会改变, 该发请求的地方并没有发请求, 所以我还是选择了自己搭建一个本地的服务, 通过控制路由返回不同的 mock 数据来处理, 并且通过为请求增加一个额外 mock 参数通知业务哪些接口应当被自建的 mock-server 拦截, 从而尽量减少对原有业务的影响.
在 mock-server 开发之前, 我们需要明白我们的 mock-server 应当能做哪些事情:
所改即所得, 具有热更新的能力, 每次增加 / 修改 mock 接口时不需要重启 mock 服务, 更不用重启前端构建服务
mock 数据可以由工具生成不需要自己手动写
能模拟 POST,GET 请求
因为 mock 的模拟数据都在本地维护, 我们所需要的只要是个无界面的能够响应请求的 server 即可, 所以我选择了 JSON-server
在构建 server 之前, 我们先要明确我们需要模拟的数据是什么, 以及用什么 (mockjs) 去维护
- // db.JS
- var Mock = require('mockjs');
- // 通过使用 mock.JS, 来避免手写数据
- module.exports = {
- getComment: Mock.mock({
- "error": 0,
- "message": "success",
- "result|40": [{
- "author": "@name",
- "comment": "@cparagraph",
- "date": "@datetime"
- }]
- })
- };
其次我们要知道我们跳转的访问路由是哪些:
- // routes.JS
- // 根据 db.JS 中的 key 值, 自动生成的路由便是 /[key], 在 route.JS 中的声明只是为了重定向
- module.exports = {
- "/comment/get": "/getComment"
- }
然后我们就可以书写我们启动 server 的主要代码了:
- // server.JS
- const jsonServer = require('json-server')
- const db = require('./db.js')
- const routes = require('./routes.js')
- const port = 3000;
- const server = jsonServer.create()
- // 使用 mock 的数据生成对应的路由
- const router = jsonServer.router(db)
- const middlewares = jsonServer.defaults()
- // 根据路由列表重写路由
- const rewriter = jsonServer.rewriter(routes)
- server.use(middlewares)
- // 将 POST 请求转为 GET, 满足可以接受 POST 和 GET 请求的需求
- server.use((request, res, next) => {
- request.method = 'GET';
- next();
- })
- server.use(rewriter) // 注意: rewriter 的设置一定要在 router 设置之前
- server.use(router)
- server.listen(port, () => {
- console.log('open mock server at localhost:' + port)
- })
由此, 只要使用 node server.JS 便能够启动一个 mock-server 了, 但是这样启动的 server, 并不能因为我修改 route.JS 或者 db.JS 而实时更新, 也就是说, 我需要每次都重启一次才能更新我的 server, 这里还需要我们进行一个小操作, 比如使用 nodemon 来监控我们的 mock-server.
- // 将所有和 mock 相关的文件: db.JS route.JS server.JS 放入 mock 文件夹
- // 然后执行:
- $ nodemon --watch mock mock/server.JS
- // 就能够启动一个能自动热更新的 mock-server 了.
这之后, 我们只需要在自己的业务代码中, 使用我们之前定义的类似于 getDataUseMock 的方法, 就可以对指定 API 进行 mock 啦.
虽然我们这样做已经完成了 mock 数据和业务代码的完全分离, 但是还是不可避免的在业务代码中使用了特殊的方法来声明我需要 mock 某个接口, 还是同样要面对当不需要 mock 时, 要删除这些方法并替换成正式请求的方法的问题. 而且 mock 数据的部分仍然放在和业务代码一个 Git 目录下, 只有开发者才有权限去修改和增加, 并没有很好地达到 mock 应当有的作用.
为此, 我征求了部门 Leader 和 "广大" 开发者的意见, 确定了我们需要的 mock 应当是怎样的:
尽量少的修改业务中的代码就能使用 mock
修改的业务代码不会影响正常的业务流程
mock-server 应当是面向所有人, 而不只是前端开发者
能够可视化的修改和增加 mock 接口和 mock 数据
能够同时支持多个项目使用
在这几个基本原则的帮助下, 我们的 mock 终于晋级到了 "永恒钻石" 段位.
"永恒钻石"
在钻石段位的加持下, 我找到了 mock-server 的 "上分利器": 来自阿里前端团队开源的 THX 工具库 https://thx.github.io/ 中的 RAP2, 其包含的优势完全符合我对 mock 的需求. 在依照网上的教程 https://www.cnblogs.com/rynxiao/p/9080179.html , 将 RAP2 部署到了我们本地的服务器上之后, 我们只需要通过在本地配置 hosts 文件即可访问我们自己的 RAP2, 这之后, 我们需要做的仅仅只剩下业务代码中的处理了:
尽量少的修改业务中的代码就能使用 mock
修改的业务代码不会影响正常的业务流程
为了能够尽量少的去修改代码并且让修改的代码不影响正常的业务流程, 我们需要增加一个特殊的开发模式, 仅在这个开发模式下, 我们修改的代码才会生效, 或者说才会存在.
我们给我们新增的开发模式可以命名为 mock 开发模式, 为了区分这个开发模式, 我们使用 Node.JS 中的环境变量来进行区分.
- "scripts": {
- "dev:mock": "cross-env MOCK=true npm run dev"
- }
在使用 cross-env 声明了环境变量之后, 我们可以通过 process.env.MOCK 获取到我们声明的环境变量的值, 当我们增加的 MOCK 变量存在, 且为 true 时, 我们才进行 mock 的请求拦截.
但是我们仅仅声明这一点还是不够, 我们还需要通知业务代码, 哪些接口需要被 mock. 所以, 我们还需要一个 mock 模式下才会存在的列表, 来告诉我们哪些接口应当被 mock.
- // config.JS
- if (process.env.MOCK) {
- config.mockList = [
- '/api/test',
- '/api/needMock'
- ]
- } else {
- config.mockList = [];
- }
当然你也可以使用条件编译来判断是否将 config.mockList 打入你的代码里, 这是更加好的选择.
接下来, 你只需要在你封装的请求方法里, 对 config 的 mockList 和你当前请求的 API 进行对比, 判断其是否要进行 mock 即可.
- import config from '../config/config';
- const mockServer = 'http://rap2.xxx.com'
- function request(opt) {
- const apiName = opt.API;
- if (config.mockList && config.mockList.includes(apiName)) {
- opt.url = `${mockServer}/${apiName}`;
- }
- ...
- }
如此, 我们的 mock 终于到达了最终形态, 从此只要接口文档(甚至 RAP2 的 mock 接口就可以直接作为接口文档), 我们就能随意的进行开发测试啦~
RAP2 的使用
从团队开始
团队是仓库的上级单位, 一个团队可以拥有多个 mock 仓库, 但是不是只有团队才能拥有仓库, 个人也可以. 使用团队的目的只是为了让团队下的仓库不被团队外人员获悉, 保持一个团队的私密性(当然你也可以选择公开团队).
仓库
仓库是接口的上级单位, 可以归属于个人或者团队, 每个仓库都可以指派开发人员, 被指定的人员可以修改或者添加仓库的接口, 未被指派的人员仅能查看接口, 每个仓库都拥有一个特定的仓库域名前缀. 其下的接口域名规则都遵循:${仓库前缀域名}${接口配置域名}, 且每个仓库都提供一个接口获取当前仓库数据.
接口
我们先来看看接口配置页面的组成:
可以看到接口页面主要由如下部分组成:
新建接口(接口列表)
接口模块
接口详情(请求参数和响应参数)
在接口详情中, 请求的 mock 接口的路由是在新建接口的时候去创建的, 创建之后自动生成一个接口, 请求地址就是 ${仓库域名}${接口路由}.
请求参数的部分配置我们最主要要关注的是生成规则和默认值, 其规则和模板可以参考 mockJs 的文档中的语法规范, 生成规则遵循数据模板定义规范(Data Template Definition,DTD), 默认值遵循数据占位符定义规范(Data Placeholder Definition,DPD).
引用内容
Mock 测试, 何去何从 https://www.wenji8.com/p/44aKIXY.html
纯手工打造前端后端分离项目中的 mock-server https://www.v2ex.com/t/365568
教你使用 docker 部署淘宝 rap2 服务 https://www.cnblogs.com/rynxiao/p/9080179.html
来源: https://juejin.im/post/5bd82d796fb9a05d25682a66