浅谈Mock的应用

阅读之前

希望你能有以下基础,方便阅读:

  • ECMAScript 6 (ES6)

为什么需要Mock

image

这样的场景,相信大家会觉得似曾相识。

现今的业务系统已经很少是孤立存在的了,尤其对于一个大公司而言,各个部门之间的配合非常密切,我们或多或少都需要使用兄弟团队或是其他公司提供的接口服务。这样的话,就对我们的联调和测试造成了很大的麻烦。假如各个兄弟部门的步伐完全一致,那么问题就会少很多,但理想很丰满,现实却很骨感,要做到步伐一致基本是不可能的。

为此,我们就需要使用一些工具来帮助我们将业务单元之间尽量解耦,它就是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,通过使用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工具库中的RAP2,其包含的优势完全符合我对mock的需求。在依照网上的教程,将RAP2部署到了我们本地的服务器上之后,我们只需要通过在本地配置 hosts 文件即可访问我们自己的RAP2,这之后,我们需要做的仅仅只剩下业务代码中的处理了:

  • 尽量少的修改业务中的代码就能使用mock
  • 修改的业务代码不会影响正常的业务流程

为了能够尽量少的去修改代码并且让修改的代码不影响正常的业务流程,我们需要增加一个特殊的开发模式,仅在这个开发模式下,我们修改的代码才会生效,或者说才会存在。

我们给我们新增的开发模式可以命名为mock开发模式,为了区分这个开发模式,我们使用nodejs中的环境变量来进行区分。

"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仓库,但是不是只有团队才能拥有仓库,个人也可以。使用团队的目的只是为了让团队下的仓库不被团队外人员获悉,保持一个团队的私密性(当然你也可以选择公开团队)。

仓库

仓库是接口的上级单位,可以归属于个人或者团队,每个仓库都可以指派开发人员,被指定的人员可以修改或者添加仓库的接口,未被指派的人员仅能查看接口,每个仓库都拥有一个特定的仓库域名前缀。其下的接口域名规则都遵循:${仓库前缀域名}${接口配置域名},且每个仓库都提供一个接口获取当前仓库数据。

接口

我们先来看看接口配置页面的组成:


image

可以看到接口页面主要由如下部分组成:

  • 新建接口(接口列表)
  • 接口模块
  • 接口详情(请求参数和响应参数)

在接口详情中,请求的mock接口的路由是在新建接口的时候去创建的,创建之后自动生成一个接口,请求地址就是${仓库域名}${接口路由}

请求参数的部分配置我们最主要要关注的是生成规则和默认值,其规则和模板可以参考mockJs文档中的语法规范,生成规则遵循数据模板定义规范(Data Template Definition,DTD),默认值遵循数据占位符定义规范(Data Placeholder Definition,DPD)

引用内容

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,734评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,699评论 2 59
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,628评论 18 139
  • 感觉今天过得特别快,上午在家里忙着洗衣拖地洗头发做饭,下午一点半就出发来学校,邻座的小孩一直叽叽喳喳个不停,不停问...
    若水青衫阅读 201评论 4 1