目前,我正在与KYC服务提供商进行交互。 像往常一样,没有宇宙。 您只需要从数据库中选择各种记录的相当大的副本集,将它们上传到服务提供商,然后请记录提供商对其进行检查。
处理的初始阶段包含许多相同的操作,这些操作发送请求以从各种数据库表中检索特定用户的数据。 假设在这种情况下,相当一部分代码可以重用作为Request
的抽象。 我将尝试建议如何使用它。 我将编写第一个测试:
describe('Request', () => { const Request = require('./Request'); it('execute should return promise', () => { const request = new Request(); request.execute().then((result) => { expect(result).toBeNull(); }); }); });
看起来还不错吗? 也许是不完美的,但是乍一看似乎Request
是本质上返回Promise
结果的
? 由此很有可能开始。 我将草绘代码,以便可以运行测试。
class Request { constructor(){} execute(){ return new Promise((resolve, reject) => { resolve(null); }); } } module.exports = Request;
我执行npm test
并在控制台中观察已完成测试的绿点。
这样啊 我有一个请求,可以执行。 但实际上,我将需要以某种方式告知我他的请求,即他应该在哪个表中查找必要的数据以及该数据应满足的条件。 我将尝试编写一个新测试:
it('should configure request', () => { const options = { tableName: 'users', query: { id: 1 } }; request.configure(options); expect(request.options).toEqual(options); });
好吗 我认为,相当。 由于我已经有两个使用request
变量实例的测试,因此我将以一种特殊的方法初始化该变量,该方法在每个测试开始之前就已执行。 因此,在每个测试中,我将拥有一个请求对象的新实例:
let request = null; beforeEach(() => { request = new Request(); });
我在请求类中实现了此功能,并向其添加了一个方法,该方法将设置保存在类实例变量中,如测试所示。
configure(options){ this.options = options; }
我运行测试,现在我看到两个绿色的点。 我的两个测试成功完成。 但是 但是,假设我的查询将被发送到数据库。 现在,可能值得尝试查看请求将接收有关数据库的信息的哪一边。 我将返回测试并编写一些代码:
const DbMock = require('./DbMock'); let db = null; beforeEach(() => { db = new DbMock(); request = new Request(db); });
在我看来,这种通过构造函数进行初始化的经典版本完全可以满足我目前的要求。
自然,我不会使用单元测试来使用与我们的项目一起使用的真实MySQL数据库的接口。 怎么了 因为:
- 如果不是我,而是我的一位同事需要在项目的这一部分上工作并执行单元测试,那么在他们无法执行任何操作之前,他们将不得不花费时间和精力来安装和设置自己的MySQL服务器实例。
- 单元测试的成功将取决于MySQL服务器数据库使用的初步数据填充的正确性。
- 使用MySQL数据库运行测试的时间将明显更长。
知道了 例如,为什么在单元测试中不使用内存中的任何数据库? 它可以快速运行,并且其配置和初始化过程可以自动化。 没错,但是目前我看不到使用此附加工具有任何好处。 在我看来,使用
和
对象的类和方法,我当前的需求更快,更便宜(无需花时间研究),它们只会模拟应该在战斗条件下使用的接口的行为。
对了 在战斗中,我建议将书架与knex结合使用。 怎么了 因为遵循了有关安装,配置和使用这两个工具的文档,所以我设法在几分钟内创建并执行了对数据库的查询。
随之而来的是什么? 因此,我应该修改Request
类的代码,以使Request
的执行与我的作战工具导出的接口相匹配。 因此,现在的代码应如下所示:
class Request { constructor(db){ this.db = db; } configure(options){ this.options = options; } execute(){ const table = this.db.Model.extend({ tableName: this.options.tableName }); return table.where(this.options.query).fetch(); } } module.exports = Request;
我将运行测试,看看会发生什么。 是的 当然,我没有DbMock
模块,所以我要做的第一件事就是为其实现一个存根:
class DbMock { constructor(){} } module.exports = DbMock;
我将再次运行测试。 现在呢 Jasmine
公主公主Jasmine
告诉我,我的DbMock
没有实现Model
属性。 我将尝试提出一些建议:
class DbMock { constructor(){ this.Model = { extend: () => {} }; } } module.exports = DbMock;
再次运行测试。 现在的错误是在单元测试中,我运行查询时未先使用configure
方法设置其参数。 我解决了这个问题:
const options = { tableName: 'users', query: { id: 1 } }; it('execute should return promise', () => { request.configure(options); request.execute().then((result) => { expect(result).toBeNull(); }); });
由于我已经在两个测试中使用了options
变量的实例,因此将其放入整个测试套件的初始化代码中,然后再次运行测试。
不出所料, DbMock
类的extend
方法, Model
的属性使我们返回undefined
,因此,自然地,我们的请求无法调用where
方法。
我已经知道DbMock
类的Model
属性应该在DbMock
类DbMock
之外实现。 首先,由于必须有
的实现才能运行现有测试,因此在直接在DbMock
类中初始化Model
属性时,它将需要太多嵌套作用域。 完全不可能阅读和理解...但是,这不会阻止我进行这样的尝试,因为我想确保我仍然有机会编写仅几行代码并使测试成功运行。
这样啊 吸气,呼气,心晕离开房间。 补充DbMock
构造函数的实现。 Ta-daaaammmmm ....
class DbMock { constructor(){ this.Model = { extend: () => { return { where: () => { return { fetch: () => { return new Promise((resolve, reject) => { resolve(null); }); } }; } }; } }; } } module.exports = DbMock;
锡! 但是,请用坚定的手进行测试,并确保Jasmine
再次向我们显示绿点。 这意味着我们仍处于正确的轨道上,尽管有些事情不适当地肿了。
接下来是什么? 肉眼可以看出,伪数据库的Model
属性必须实现为完全独立的东西。 虽然是暂时的,但尚不清楚应如何实施。
但是我可以肯定的是,现在我将以最普通的数组将记录存储在此伪数据库中。 由于对于现有测试,我只需要模拟users
表,因此从头开始,我将实现一个带有一个记录的用户数组。 但首先,我要编写一个测试:
describe('Users', () => { const users = require('./Users'); it('should contain one user', () => { expect(Array.isArray(users)).toBeTruthy(); expect(users.length).toEqual(1); const user = users[0]; expect(user.Id).toEqual(1); expect(user.Name).toEqual('Jack'); }); });
我进行测试。 我确保它们不会通过,并与用户一起实现我的简单容器:
const Users = [ { Id: 1, Name: 'Jack' } ]; module.exports = Users;
现在正在执行测试,在我看来,书bookshell
包中的Model
在语义上是访问数据库中表内容的接口的提供者。 毫无疑问,我们将带有表名的对象传递给extend
方法。 我不知道为什么称它为extend
,而不是例如get
。 也许这只是缺乏对bookshell
API的了解。
好吧,上帝保佑他,现在我对以下测试主题有了一个想法:
describe('TableMock', () => { const container = require('./Users'); const Table = require('./TableMock'); const users = new Table(container); it('should return first item', () => { users.fetch({ Id: 1 }).then((item) => { expect(item.Id).toEqual(1); expect(item.Name).toEqual('Jack'); }); }); });
由于目前需要一个仅模拟真实存储驱动程序功能的实现,因此我相应地命名了类,并添加了后缀Mock
:
class TableMock { constructor(container){ this.container = container; } fetch() { return new Promise((resolve, reject) => { resolve(this.container[0]); }); } } module.exports = TableMock;
但是fetch
并不是我打算在战斗版本中使用的唯一方法,因此我添加了一个测试:
it('where-fetch chain should return first item', () => { users.where({ Id: 1 }).fetch().then((item)=> { expect(item.Id).toEqual(1); expect(item.Name).toEqual('Jack'); }); });
如预期的那样,启动它会向我显示错误消息。 因此,我使用where
方法补充了TableMock
的实现:
where(){ return this; }
现在,已经执行了测试,您可以继续执行DbMock
类中实现Model
属性的主题。 正如我已经建议的,这将是TableMock
类型的对象实例的某个提供程序:
describe('TableMockMap', () => { const TableMock = require('./TableMock'); const TableMockMap = require('./TableMockMap'); const map = new TableMockMap(); it('extend should return existent TableMock', () => { const users = map.extend({tableName: 'users'}); expect(users instanceof TableMock).toBeTruthy(); }); });
为什么选择TableMockMap
,因为从语义上来说就是这样。 仅使用extend
方法名称代替get
方法名称。
由于测试崩溃,因此我们实现了:
const Users = require('./Users'); const TableMock = require('./TableMock'); class TableMockMap extends Map{ constructor(){ super(); this.set('users', Users); } extend(options){ const container = this.get(options.tableName); return new TableMock(container); } } module.exports = TableMockMap;
运行测试,并在控制台中看到六个绿点。 生活是美好的。
在我看来,现在您已经可以使用出色的TableMockMap
摆脱DbMock
类的构造函数中的
初始化
了。 我们不会推迟,特别是因为已经喝茶会很好。 新的实现非常优雅:
const TableMockMap = require('./TableMockMap'); class DbMock { constructor(){ this.Model = new TableMockMap(); } } module.exports = DbMock;
运行测试……哎呀! 我们最重要的考验是跌落。 但这甚至很好,因为它是一个测试存根,现在我们只需要修复它:
it('execute should return promise', () => { request.configure(options); request.execute().then((result) => { expect(result.Id).toEqual(1); expect(result.Name).toEqual('Jack'); }); });
测试成功完成。 现在您可以稍作休息,然后返回到完成最终的请求代码,因为它仍然离完美还非常非常遥远,即使是简单易用的界面也是如此,尽管使用它的数据来自基地已经可以收到。