你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
zswang

用代码块来模拟接口 5个回复 专栏 @ 工具

zswang 发布于 1 年前

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


背景

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

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

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

什么是模拟接口?

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

为什么要做模拟接口?

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

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

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

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

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

现有方案

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

通常需要借助测试框架和模拟环境(如: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);
?>

参考

  • wonder

    有新意

    #1
  • wonder

    webpack loader呢

    #2
  • zswang
    #3
  • jincdream

    POST的就基本不行了吧。如果接口过于复杂的话,模拟起来也很蛋疼。

    #4
  • rambo

    通过注释来达到模拟 已经在使用了。
    我们是将 本地开发(测试) 的代码, 写在一个文件中,然后通过requirejs来引进来, 把上面的正常代码改写,比如您的那个改写getJson方法。 最后上线的时候只需要从requirjs这个测试module给移除

    #5
登录后回复,如无账号,请使用邀请码注册