11月阅读周·编写可测试的JavaScript代码:测试基于事件的架构篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)