11月阅读周·编写可测试的JavaScript代码:测试基于事件的架构篇

举报
叶一一 发表于 2024/11/21 10:44:19 2024/11/21
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》

当前阅读周书籍编写可测试的JavaScript代码

测试基于事件的架构

测试基于事件的架构,仅仅涉及实现功能函数的调用。下面是一个有趣的示例:

// Some handle to a datastore
function DB(eventHub, dbHandle) {
  // add user function
  eventHub.on('CREATE_USER', dbHandle);
  function addUser(user) {
    var result = dbHandle.addrow('user', user);
    eventHub.fire('USER_CREATED', {
      success: result.success,
      message: result.message,
      user: user,
    });
  }
}

上述代码有几个注意事项。首先,没有回调,并且事件用于广播该操作的成功或失败。大家可能想使用一个回调,以便调用者知道该操作是否成功,但是通过广播响应,我们可以让其他利害关系方知道是否已经成功创建了用户。一个用户被创建时,多个客户端可能都要通知到。这样,任何浏览器或服务器端的客户端都将得到该事件的通知。这也使得,可以让一个单独模块负责处理要创建用户的事件,而不是非得使用addUser内的代码进行创建。当然。创建代码也可以和AddUser处于同一个文件,但没必要非得这样,这种方式有助于实现极大的模块化。事实上,通常服务器端模块和客户端模块都想知道用户是否被创建,并据此采取相应的行动。客户端监听器可以据此更新相应的UI,而服务器端监听器则可以更新其他相应的内部结构。

在上述代码中,eventHub和databaseHandle被注入到了构造函数中,以协助测试和模块化。没有对象被创建,只有事件监听器被注册到eventHub。这就是基于事件架构的本质:注册事件监听器,并且没有(或很少)对象被实例化。

基于浏览器的处理程序,看起来可能是下面这样:

eventHub.on('USER_CREATED', function (data) {
  dialog.show('USER_CREATED:' + data.success);
});

而服务器端的处理程序,看起来可能是如下这样:

eventHub.on('USER_CREATED', function (data) {
  console.log('USER_CREATED:' + data.success);
});

任何时候,任何人试图从当前浏览器、其他浏览器甚至服务器端上创建新用户时,该浏览器的代码都会弹出一个对话框。如果这就是你想要的,方案就是让该事件作为广播。如果不是,那就使用回调。

在事件中,还可以将事件结果传递给回调或者另外一个事件(甚至两个都可以)。如下是客户端代码示例:

eventHub.fire('CREATE_USER', user);

如下是服务器端代码:

eventHub.fire('CREATE_USER', user);

上述代码再简单不过了。

要测试该函数,可以使用一个模拟事件集线器和局部数据库进行处理,以验证事件是否正常触发:

YUI().use('test', function (Y) {
  var eventHub = Y.Mock();
  var addUserTests = new Y.Test.Case({
    name: 'add user',
    addOne: function () {
      var user = { user_id: 'mark' };
      var dbHandler = {
        // DB stub
        addRow: function (user) {
          return {
            user: user,
            success: true,
            message: 'ok',
          };
        },
      };
      DB(eventHub, dbHandler); // Inject test versions
      Y.Mock.expect(eventHub, 'fire', ['USER_CREATED', { user: user, success: true, message: 'ok' }]);
      addUser(user);
      Y.Mock.verify(eventHub);
    },
  });
  Y.Test.Runner.add(addUserTests);
  Y.Test.Runner.run();
});

该测试在测试addUser函数时,既用了模对象(针对事件集线器),也用了桩对象(针对DB处理程序)。通过addUser函数和相应的参数,eventHub对象上的fire事件将会被触发。DB对象上的addRow函数将返回封装数据。这两个对象注入到DB对象中进行测试,然后开始运行。

将它与更标准的方法进行比较,该标准方法是将带有addUser原型方法的DB对象进行实例化,示例如下:

var DB = function (dbHandle) {
  this.handle = dbHandle;
};
DB.prototype.addUser = function (user) {
  var result = dbHandle.addRow('user', user);
  return {
    success: result.success,
    message: result.message,
    user: user,
  };
};

那么客户端代码如何访问该函数呢?

transporter.sendMessage('addUser', user);

全局或集中式的消息传递机制将使用Ajax或同等技术把消息发送到服务器上。一般是在客户端更新或创建一个实例化用户模型对象以后才这样做。这就需要对请求和响应进行跟踪,并且服务器端代码需要将消息路由到全局的单一DB对象上,然后进行响应,并将响应发回至客户端。

如下是对上述代码的测试示例:

YUI().use('test', function (Y) {
  var eventHub = Y.Mock();
  var addUserTests = new Y.Test.Case({
    name: 'add user',
    addOne: function () {
      var user = { user_id: 'mark' };
      var dbHandler = {
        // DB stub
        addRow: function (user) {
          return {
            user: user,
            success: true,
            message: 'ok',
          };
        },
      };
      DB(eventHub, dbHandler); // Inject test versions
      Y.Mock.expect(eventHub, 'fire', ['USER_CREATED', { user: user, success: true, message: 'ok' }]);
      addUser(user);
      Y.Mock.verify(eventHub);
    },
  });
  Y.Test.Runner.add(addUserTests);
  Y.Test.Runner.run();
});

这种情况,DB对象没有依赖关系,所以测试代码很类似。

对于添加新用户的客户端来说,客户端和服务器之间必须创建新的链接协议。必须在服务器中创建一个路由,用以响应DB对象中的“添加用户”消息。其结果必须序列化并返回给调用者,需要为每个消息类型重新创建大量的通信管道。最终,你会得到一个拼凑的RPC系统作为事件集线器而随意使用。

这种代码就是一个很好的例子,有85%的样板代码我们不需要自己进行实现了。

总结

所有的应用程序都归结为消息传递和作业控制。应用程序传递消息,并等待回复。对应用程序的样板代码进行编写和测试会增加很多开销,这是不必要的开销。如果还想让应用程序有命令行界面怎么办?这是另一个必须要实现和维护的代码路径了。使用事件集线器就可以达到该目的。


作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。