转载

用代码块来模拟接口

标签: jdists 模拟接口 单元测试

背景

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

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

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

什么是模拟接口?

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

为什么要做模拟接口?

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

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

  • 异步开发接口开发过程不用等到另一端开发完毕

  • 用例覆盖可以预设输出结果,覆盖各种极端用例

  • 降低测试成本减少手动操作步骤、减少搭建环境的时间和物资

现有方案

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

通常需要借助测试框架和模拟环境(如: jest 、 phantomjs )

用例: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); ?> 

参考

原文  http://div.io/topic/1612
正文到此结束
Loading...