用代码块来模拟接口

背景

应用的功能模块之间,离不开接口的设计和实现,接口通常涉及:

  • 模块与模块之间
  • 前端和后端之间
  • 内置页面和客户端之间

本文介绍一种模拟接口的简单方案

什么是模拟接口?

就是伪造接口的功能,忽略实现细节,模拟调用过程和结果。

为什么要做模拟接口?

项目中两个需对接的功能模块,开发周期和复杂度并不会一样。有的模块开发几小时就完了,而一些模块要写好几周。
不同的功能模块需要的运行环境也有差异,一些需要数据库,一些需要运行在微信环境,一些需要硬件支持。

模拟接口对于开发期的收益显而易见:

  • 异步开发
    接口开发过程不用等到另一端开发完毕
  • 用例覆盖
    可以预设输出结果,覆盖各种极端用例
  • 降低测试成本
    减少手动操作步骤、减少搭建环境的时间和物资

现有方案

方案一:劫持接口来模拟接口调用

通常需要借助测试框架和模拟环境(如:jestphantomjs

用例:jQuery Ajax 测试

来源:tutorial-jquery.html

业务代码 fetchCurrentUser.js

function fetchCurrentUser(callback) { return $.ajax({ type: 'GET', url: 'http://example.com/currentUser', done: user => callback(parseJSON(user)), }); }

测试代码 displayUser-test.js

jest .dontMock('../displayUser.js') .dontMock('jquery'); describe('displayUser', function() { it('displays a user after a click', function() { // ... var $ = require('jquery'); var fetchCurrentUser = require('../fetchCurrentUser'); // Tell the fetchCurrentUser mock function to automatically invoke // its callback with some data fetchCurrentUser.mockImplementation(function(cb) { cb({ loggedIn: true, fullName: 'Johnny Cash' }); }); // ... }); });

这个用例中 fetchCurrentUser.mockImplementation() 执行后,原函数就被劫持,执行回调结果:

{ loggedIn: true, fullName: 'Johnny Cash' }

当然这样就不用等待 http://example.com/currentUser 接口实现

方案二:实现对接方的接口功能

这是成本相对较高的方法,将对方的接口做一次简单实现,返回固定的模拟数据。
如果对接方能提供是最理想的。

现有方案的不足

  • 不容易维护,测试代码和源代码是分离的;
  • 模拟网络请求的方案比较多,但模拟 Native 的方案较少;
  • 功能难以串联在一起,不能完整体验一个应用的功能。

新思路-代码块处理模拟接口

无论采用什么方案,都得写一段模拟接口的代码,为何不写在距离接口调用最近的地方?

业务代码调用接口的地方就是最近的地方。

这就是本文提出的方案:在业务代码中注入模拟接口代码。

比如 jest 中 jQuery 的用例,修改成这样:

开发期

function fetchCurrentUser(callback) { ////////////// callback({ loggedIn: true, fullName: 'Johnny Cash' }); return; ////////////// return $.ajax({ type: 'GET', url: 'http://example.com/currentUser', done: user => callback(parseJSON(user)), }); }

生产环境

function fetchCurrentUser(callback) { return $.ajax({ type: 'GET', url: 'http://example.com/currentUser', done: user => callback(parseJSON(user)), }); }

在上线的时候把这段模拟接口的代码移除。

这种简单的模拟接口方式为何不常见?
我想可能是在构建工具没有普及的时代,手动增删不够方便。
利用构建工具这个过程就可以变成自动的。
只需要做一下标记即可。

带来的新问题

  • 怎么标记需要移除的代码块
/*<remove>*/ ... 业务代码 ... /*</remove>*/

我推荐的是 jdists 使用的方法:「多行注释 + XML」。

  • 移除代码需要依赖构建工具

现在构建工具已经很普及 Gulp、Grunt、FIS,jdists 提供这些构建工具的插件,可以方便的引入。

当然也可以简单的用 replace 写个正则,替换一下。

场景

  • 模拟 jQuery Ajax
var user = $('.userid').val(); $.getJSON('/user/info/' + user, function (reply) { if (reply.status == 'ok') { $('.nickname').text(reply.data.nickname); } });
var user = $('.userid').val(); /*<remove>*/ $.oldGetJSON = $.getJSON; $.getJSON = function (url, callback) { // 接管接口功能 console.log('url: %s', url); callback({ status: 'ok', data: { id: 4455, nickname: 'zswang' } }); }); /*</remove>*/ $.getJSON('/user/info/' + user, function (reply) { if (reply.status == 'ok') { $('.nickname').text(reply.data.nickname); } }); /*<remove>*/ $.getJSON = $.oldGetJSON; // 恢复接口功能 /*</remove>*/
  • 模拟 Native 调用
/*<remove>*/ JavaScriptBridge.oldInvoke = JavaScriptBridge.invoke; JavaScriptBridge.invoke = function (action, query, callback) { console.log('invoke() action: %s, query: %s', action, JSON.stringify(query)); callback({ status: 'ok' }); }; /*</remove>*/ try { JavaScriptBridge.invoke("wechat_share", { title: document.title, icon: $('img').attr('src') }, function(reply) { if (reply.status === 'ok') { alert('分享成功'); } } ); } catch(ex) {} /*<remove>*/ JavaScriptBridge.invoke = JavaScriptBridge.oldInvoke; /*</remove>*/
  • 模拟微信接口
/*<remove>*/ wx.ready = function(callback) { callback(); }; /*</remove>*/ wx.ready(function() { /*<remove>*/ wx.onMenuShareTimeline = function (argv) { console.log(JSON.stringify(argv)); if (Math.random() < 0.5) { argv.success(); } else { argv.cancel(); } }; /*</remove>*/ // 获取“分享到朋友圈”按钮点击状态及自定义分享内容接口 wx.onMenuShareTimeline({ title: document.title, // 分享标题 link: document.location, // 分享链接 imgUrl: $('.icon').attr('src'), // 分享图标 success: function () { // 用户确认分享后执行的回调函数 $('.info').text('分享成功'); }, cancel: function () { // 用户取消分享后执行的回调函数 $('.info').text('分享识别'); } }); });
  • 模拟 MySQL 查询

jdists 提供了触发器,可以选择哪些情况需要移除。

<?php /*<remove trigger="local">*/ $db = new mysqli( Config::mysql_database_host, Config::mysql_database_user, Config::mysql_database_pass, Config::mysql_database_db, Config::mysql_database_port ); $owner_id = 4455; $res = $db->query(" SELECT `id`, `nickname`, `city` FROM `visits` WHERE `owner_id` = $owner_id ORDER BY `id` DESC "); $db->close(); $visits = array(); if ($res) { while ($row = $res->fetch_object()) { $visits[] = $row; } } /*</remove>*/ /*<remove trigger="@trigger != 'local'">*/ $visits = json_decode('[ {"id": 4451, "nickname": "zswang", "city": "beijing"}, {"id": 4452, "nickname": "techird", "city": "guangzhou"} ]'); /*</remove>*/ var_dump($visits); ?>

参考

  1. da shang
    donate-alipay
               donate-weixin weixinpay

发表评论↓↓