如何避免重新实现功能以避免双重测试
how to avoid re-implementing functionality to avoid double testing
我们使用 TDD 开发了一个组件。该组件有一个持久性后端。
我们选择使用 SQLite 来实现它。
后端可以 Save() 和 Load() 项目的集合。
因此,为了编写测试 Load() 函数,我们在测试中用 SQL 代码填充了 SQLite 数据库,因此我们不使用 Save() 函数,这样我们就不会对其进行测试Load() 测试。
所以基本上我们从我们的测试中重新实现了代码,这些代码也在我们的组件中。
就像我说的,我们这样做是为了只能测试孤立的功能。
这有一种(非常)酸的味道。
- 重新实现生产代码
- 测试取决于实现细节SQLite
我的问题是,为避免同时测试我们的 Load() 和 Save() 函数而付出这样的努力是否值得?
我们的 Whosebug 成员是否使用了其他方法?
最终取决于你,总有一个权衡。我个人的想法...
对于单元测试,将它们作为两个单独的测试用例(单一职责)可能是值得的。这使测试更易于阅读和维护,并且还将有助于隔离测试失败,即您将知道它是否在加载或保存期间失败。
尽管通过测试预期的行为而不是单独的方法,这可能会提供不同的视角
这是一个单元测试,然后避免实际写入数据库,也许可以考虑使用内存版本。
在集成或系统测试中使用真实数据库 - 这些可以更高级别,您可以将读取和写入测试组合到一个功能或场景中(测试金字塔)。
如果您的测试代码中存在重复,请尝试通过使用可重复用于设置测试的测试数据构建器来删除它(DRY 原则)。
还有一点,想想你的测试实际上在测试什么,也就是说,听起来你可能正在测试 SQLite 是否可以读写数据,而你可能需要测试输出的数据是否正确,所以使用老化Unit/Integration 测试组合,您可以在单元测试级别从数据库中提取 - 所以测试预期的行为而不是实现。
So basically we re-implemented code from our tests that is
also in our component
在我看来这不是真的。
虽然 "persistence backend" 必须是通用的(即可以为每个客户保存数据),但测试中的 sql 代码不能是通用的。测试代码中有一个具体的例子就足够了。
testLoadExistingCustomer() {
sql.exec("delete from customers where id=1");
sql.exec("Insert into Customers(id,name) values(1, 'Smith')");
Customer cust = repository.loadByid(1);
assert(.....)
}
testSaveNewCustomer() {
sql.exec("delete from customers where id=1");
Customer cust = new Customer(1, "Smith");
repository.save(customer);
int count = sql.exec("select count(*) from Customers where id=1 and name='Smith'");
assert(1,count)
}
[评论后更新]
我的回答是关于你原来的代码重复问题的白盒测试。这是黑盒测试的替代方案
我通常通过验证两个反函数 save+load 得到相同的内容来测试我的 persistence/serialisation。因为我还实现了一个 toString 方法来调试 puposes 这样的测试变成:
testSavePlusLoadCustomer() {
Customer cust = new Customer(1, "Smith");
repository.save(customer);
Customer loadedCustomer = repository.loadByid(1);
assertEquals(cust.toString(), loadedCustomer.toString());
}
这并不干净,因为此测试(与许多其他集成测试一样)有多种失败原因或误报(保存或加载工作不正常,测试数据缺少一个属性集,toString() 缺少一个输出中的属性)但它作为回归测试很好(确保以前工作的代码没有被破坏)
My question is, is it worth it to make such an effort to avoid testing our Load() and Save() functions together?
可能不会。
有不同的、成本较低的方法来分离加载和保存测试。
最常见的一种是将测试的重点从数据转移到编排。所以你会用一个测试替身来代表你的数据存储,当单元测试加载时你会注意是否将正确的消息发送到测试替身,而不用担心存储时应该发送的消息数据。同样,在对 Store 进行单元测试时,您会专注于是否发送了这些消息,而忽略了加载消息。
这是使用端口和适配器设计时的常用方法。你安排你的代码,使得难以测试的部分是 "so simple that there are obviously no deficiencies",然后用 易于测试的测试替身替换该部分。
稍后,当您进行集成或端到端测试时,您连接到一个真实的数据存储并确保您确实可以读出您正在写入的数据。
但如果这没有意义,或者仍然太昂贵,或者需要在其他地方进行不充分的权衡,那就不要这样做。
我们使用 TDD 开发了一个组件。该组件有一个持久性后端。 我们选择使用 SQLite 来实现它。 后端可以 Save() 和 Load() 项目的集合。 因此,为了编写测试 Load() 函数,我们在测试中用 SQL 代码填充了 SQLite 数据库,因此我们不使用 Save() 函数,这样我们就不会对其进行测试Load() 测试。 所以基本上我们从我们的测试中重新实现了代码,这些代码也在我们的组件中。 就像我说的,我们这样做是为了只能测试孤立的功能。
这有一种(非常)酸的味道。
- 重新实现生产代码
- 测试取决于实现细节SQLite
我的问题是,为避免同时测试我们的 Load() 和 Save() 函数而付出这样的努力是否值得?
我们的 Whosebug 成员是否使用了其他方法?
最终取决于你,总有一个权衡。我个人的想法...
对于单元测试,将它们作为两个单独的测试用例(单一职责)可能是值得的。这使测试更易于阅读和维护,并且还将有助于隔离测试失败,即您将知道它是否在加载或保存期间失败。 尽管通过测试预期的行为而不是单独的方法,这可能会提供不同的视角
这是一个单元测试,然后避免实际写入数据库,也许可以考虑使用内存版本。
在集成或系统测试中使用真实数据库 - 这些可以更高级别,您可以将读取和写入测试组合到一个功能或场景中(测试金字塔)。
如果您的测试代码中存在重复,请尝试通过使用可重复用于设置测试的测试数据构建器来删除它(DRY 原则)。
还有一点,想想你的测试实际上在测试什么,也就是说,听起来你可能正在测试 SQLite 是否可以读写数据,而你可能需要测试输出的数据是否正确,所以使用老化Unit/Integration 测试组合,您可以在单元测试级别从数据库中提取 - 所以测试预期的行为而不是实现。
So basically we re-implemented code from our tests that is also in our component
在我看来这不是真的。
虽然 "persistence backend" 必须是通用的(即可以为每个客户保存数据),但测试中的 sql 代码不能是通用的。测试代码中有一个具体的例子就足够了。
testLoadExistingCustomer() {
sql.exec("delete from customers where id=1");
sql.exec("Insert into Customers(id,name) values(1, 'Smith')");
Customer cust = repository.loadByid(1);
assert(.....)
}
testSaveNewCustomer() {
sql.exec("delete from customers where id=1");
Customer cust = new Customer(1, "Smith");
repository.save(customer);
int count = sql.exec("select count(*) from Customers where id=1 and name='Smith'");
assert(1,count)
}
[评论后更新]
我的回答是关于你原来的代码重复问题的白盒测试。这是黑盒测试的替代方案
我通常通过验证两个反函数 save+load 得到相同的内容来测试我的 persistence/serialisation。因为我还实现了一个 toString 方法来调试 puposes 这样的测试变成:
testSavePlusLoadCustomer() {
Customer cust = new Customer(1, "Smith");
repository.save(customer);
Customer loadedCustomer = repository.loadByid(1);
assertEquals(cust.toString(), loadedCustomer.toString());
}
这并不干净,因为此测试(与许多其他集成测试一样)有多种失败原因或误报(保存或加载工作不正常,测试数据缺少一个属性集,toString() 缺少一个输出中的属性)但它作为回归测试很好(确保以前工作的代码没有被破坏)
My question is, is it worth it to make such an effort to avoid testing our Load() and Save() functions together?
可能不会。
有不同的、成本较低的方法来分离加载和保存测试。
最常见的一种是将测试的重点从数据转移到编排。所以你会用一个测试替身来代表你的数据存储,当单元测试加载时你会注意是否将正确的消息发送到测试替身,而不用担心存储时应该发送的消息数据。同样,在对 Store 进行单元测试时,您会专注于是否发送了这些消息,而忽略了加载消息。
这是使用端口和适配器设计时的常用方法。你安排你的代码,使得难以测试的部分是 "so simple that there are obviously no deficiencies",然后用 易于测试的测试替身替换该部分。
稍后,当您进行集成或端到端测试时,您连接到一个真实的数据存储并确保您确实可以读出您正在写入的数据。
但如果这没有意义,或者仍然太昂贵,或者需要在其他地方进行不充分的权衡,那就不要这样做。