我发现在某些情况下,单元测试根本无法证明任何事情。
让我们以以下(简单,幼稚的)存储库实现(在PHP中)为例:
class ProductRepository
{
private $db;
public function __construct(ConnectionInterface $db) {
$this->db = $db;
}
public function findByKeyword($keyword) {
// this might have a query builder, keyword processing, etc. - this is
// a totally naive example just to illustrate the DB dependency, mkay?
return $this->db->fetch("SELECT * FROM products p"
. " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
}
}
比方说,我想在测试中证明该存储库实际上可以找到与各种给定关键字匹配的产品。
对带有真实连接对象的集成测试的简短介绍,我怎么知道这是实际上生成了真正的查询-并且那些查询实际上按照我认为的方式做了?
如果我必须在单元测试中模拟连接对象,我只能证明“它生成了预期的东西”查询”-但这并不意味着它实际上就可以正常工作……也就是说,它可能正在生成我期望的查询,但也许该查询没有达到我认为的目的。
换句话说,我感觉像是一个测试,可以断言ab生成的查询基本上没有任何价值,因为它正在测试
findByKeyword()
方法的实现方式,但这并不能证明它确实有效。此问题不仅限于存储库或数据库集成-这似乎适用于很多情况,其中断言关于模拟的使用(test-double)只能证明事情是如何实现的,而不是它们是否会真正起作用。
您如何处理这样的情况?
这样的情况下集成测试真的“不好”吗?
我的观点是,最好测试一件事,并且我也理解为什么集成测试会导致无数的代码路径,而所有这些路径都无法测试-但就服务(例如存储库)而言,其唯一目的是要与另一个组件交互,如何在不进行集成测试的情况下真正测试任何东西?
#1 楼
编写您可以做的最小的有用测试。对于这种特殊情况,内存数据库可能会对此有所帮助。通常可以对所有可以进行单元测试的内容进行单元测试是正确的,并且您是对的,单元测试只会让您如此远远没有了,尤其是在围绕复杂的外部服务编写简单的包装程序时。
关于测试的一种常见思考方式是作为测试金字塔。这个概念经常与敏捷相关,并且包括Martin Fowler(他将其归功于“敏捷成功”中的Mike Cohn),Alistair Scott和Google Testing Blog一起写了很多。
/\ --------------
/ \ UI / End-to-End \ /
/----\ \--------/
/ \ Integration/System \ /
/--------\ \----/
/ \ Unit \ /
-------------- \/
Pyramid (good) Ice cream cone (bad)
概念是快速运行的弹性单元测试是测试过程的基础。应该有比系统/集成测试更集中的单元测试,以及比端到端测试更多的系统/集成测试。随着您越接近顶部,测试往往会花费更多的时间/资源来运行,往往会遭受更大的脆弱性和脆弱性的考验,并且在确定哪个系统或文件被破坏时的针对性也就更低。自然,最好避免过于繁琐。
在这一点上,集成测试还不错,但是严重依赖它们可能表明您并未将单个组件设计为易于测试的。请记住,这里的目标是测试您的单元是否在性能上达到了最低限度,同时涉及最少的其他易碎系统:您可能想尝试一个内存数据库(我将其视为对单元测试友好的测试,同时还模拟了模拟) ),例如进行繁重的案例测试,然后使用真实的数据库引擎编写一些集成测试,以确保在组装系统时主要案例可以正常工作。
正如您所指出的,测试的范围可能太狭窄:您提到编写的模拟程序只是测试某种东西的实现方式,而不是它是否有效。那是一种反模式:完美体现其测试的测试实际上根本没有测试任何东西。相反,请测试每个类或方法是否在其要求的抽象或真实级别下均根据其自身的规范进行操作。
从这种意义上说,您的方法的规范可能是以下内容之一:
发出一些任意的SQL或RPC,然后精确地返回结果
(模拟友好,但实际上并未测试您关心的查询)
准确地发出SQL查询或RPC,并精确地返回结果
/>(模拟友好,但很脆弱,并且假设在不进行测试的情况下SQL是可以的)
将SQL命令发布到类似的数据库引擎,并检查它是否返回正确的结果(在-memory-database-friendly,
也许是最好的平衡解决方案)
将SQL命令发布到您确切的数据库引擎的临时副本中,并检查是否返回正确的结果
(可能是一个很好的集成测试,但可能会易于发生基础结构
脆弱或难以确定错误)
向您的实际生产DB引擎发出一条SQL命令并检查
/>它会返回正确的结果
(对于检查已部署的行为,与#4相同的问题可能有用,另外
修改生产数据或使服务器不堪重负的危险)
使用您的判断:选择最快,最有弹性的解决方案,该解决方案将在您需要时失败,并让您确信解决方案是正确的。
评论
+1表示“完美体现其测试的测试根本就没有测试任何东西。”太普遍了。我称之为Doppelganger反模式。
–dodgethesteamroller
2015年11月3日,22:26
一项由上下文驱动的测试运动进行的反向软件培训QA,部分地致力于争论是否存在诸如“测试金字塔”之类的有用的通用经验法则。特别是,机芯的开创性著作提供了许多示例,其中集成测试比其他类型的测试更有价值(因为它们在上下文中测试系统,即系统)。
–dodgethesteamroller
2015年11月3日在22:32
……因此,Fowler等人认为,由于集成测试和用户接受测试太难以至于不能以健壮和可维护的方式编写,因此他们应该花更少的精力,实际上只是为事后解释提供了理由。他们还没有弄清楚如何在更高的水平上进行良好的测试。
–dodgethesteamroller
15年11月3日,22:38
@dodgethesteamroller像这样的“反向派”的深入讨论可能最适合他们自己的答案。就个人而言,我发现Google Testing Blog很好地描述了快速,范围严格的自动化测试以及系统内测试的优点。如果您不清楚,我将测试金字塔列为有用的模型或起点,而不是作为停止思考工程师的借口。
–杰夫·鲍曼(Jeff Bowman)
2015年11月3日23:01
强烈建议您介绍单元测试与集成测试的层次结构和比率:vimeo.com/80533536解释得很好。
–rszalski
2015年11月6日,19:38
#2 楼
我的一位同事坚持认为整合测试有很多坏处,都是错误的-一切都必须经过单元测试,
这有点像说抗生素是坏的-一切都应使用维生素治疗。
单元测试无法捕获所有内容-它们仅测试组件在受控环境中的工作方式。集成测试可以验证所有东西都可以一起工作,这很难完成,但最终更有意义。
一个好的,全面的测试过程使用两种类型的测试-单元测试来验证业务规则和其他可以验证的事情。可以独立进行测试,并进行集成测试以确保一切正常。
没有真正的连接对象的集成测试,我怎么知道这实际上是在生成真正的查询-并且这些查询实际上可以执行我认为的工作?
您可以在数据库级别对其进行单元测试。使用各种参数运行查询,并查看是否获得期望的结果。授予它意味着将任何更改复制/粘贴回“ true”代码中。但它确实允许您独立于任何其他依赖项来测试查询。
评论
您是否要测试数据库是否包含某些数据?
–图兰斯·科尔多瓦(TulainsCórdova)
15年11月2日在20:04
可能-但您也可能正在测试过滤器,复杂联接等是否正常工作。示例查询可能不是“单元测试”的最佳选择,但可能具有复杂的联接和/或聚合查询。
– D斯坦利
2015年11月2日在21:45
是的-正如我所指出的,我使用的示例很简单;真实的存储库可能具有各种复杂的搜索和排序选项,例如使用查询生成器等
– mindplay.dk
15年11月2日在22:02
好的答案,但是我要补充一点,数据库应该在内存中,以确保单元测试是快速的。
–BЈовић
15年11月3日,13:03
@BЈовић:不幸的是,这可能并不总是可能的,因为不幸的是,那里没有两个兼容的DB,而且它们中的所有都不能在内存中工作。商业数据库还存在许可问题(您可能没有在任何计算机上运行它的许可),...
– Matthieu M.
2015年11月3日,14:21
#3 楼
单元测试不能解决所有缺陷。但是与其他类型的测试相比,它们设置和(重新)运行的成本更低。单元测试是通过适中的价值和低至中等的成本来证明的。下表显示了各种测试的缺陷检出率。
来源:McConnell编写的Code Complete 2中的第470页
评论
该数据收集于1986年。那是30年前。 1986年的单元测试不是今天的那种。我会对此数据表示怀疑。更不用说,单元测试会在bug提交之前就对其进行检测,因此怀疑是否会报告它们。
–RubberDuck
16年6月18日在12:11
@RubberDuck此图表摘自2006年的书籍,它基于1986、1996、2002年的数据(如果您仔细看的话)。我没有研究源中的数据收集协议,也不能说它们何时开始跟踪缺陷以及如何报告缺陷。这张图表可能有点过时了吗?它可能。去年12月,我在一个研讨会上,讲师提到集成测试比单元测试发现的bug多(iirc的两倍)。该图表明它们大致相同。
–尼克·阿列克谢耶夫(Nick Alexeev)
16年6月18日在21:02
#4 楼
不,他们还不错。希望应该进行单元测试和集成测试。它们在开发周期的不同阶段使用和运行。单元测试
单元代码应在编译代码后在构建服务器上和本地运行。如果任何单元测试失败,则应该使构建失败或不提交代码更新,直到修复测试为止。我们希望将单元测试隔离的原因是,我们希望构建服务器能够在没有所有依赖项的情况下运行所有测试。然后,我们可以运行构建,而无需所有复杂的依赖关系,并且有很多运行非常快的测试。
因此,对于数据库,应该具有以下内容:
IRespository
List<Product> GetProducts<String Size, String Color);
现在,IRepository的实际实现将进入数据库以获取产品,但对于单元测试,可以使用假的产品模拟IRepository,以在不使用actaul数据库的情况下根据需要运行所有测试,因为我们可以模拟从模拟实例返回的各种产品列表,并使用模拟数据。
集成测试
集成测试通常是边界测试。我们希望在部署服务器(真实环境),沙箱甚至本地(指向沙箱)上运行这些测试。它们不在构建服务器上运行。在将软件部署到环境之后,通常这些将作为部署后活动运行。可以通过命令行实用程序将它们自动化。例如,如果我们对要调用的所有集成测试进行了分类,则可以从命令行运行nUnit。它们实际上使用真实的数据库调用来调用真实的存储库。这些类型的测试有助于:
环境健康稳定性准备情况
测试真实物体
这些测试有时很难运行,因为我们可能还需要设置和/或拆除。考虑添加产品。我们可能想添加产品,查询它是否已添加,然后在完成后将其删除。我们不想添加100或1000个“集成”产品,因此需要进行额外的设置。
集成测试可以证明对验证环境和确保真实的环境非常有价值。事情奏效。
两者都应该有。
为每个构建运行单元测试。
为每个部署运行集成测试。
评论
我建议为每个构建都运行集成测试,而不必提交和推送。取决于它们需要多长时间,但是出于许多原因,保持它们快速也是一个好主意。
– artbristol
17年1月7日在23:25
@ArtBristol-通常,我们的构建服务器没有配置完整的环境依赖项,因此我们无法在此处运行集成测试。但是,如果可以在那里进行集成测试,那就去做吧。我们在构建后设置了一个部署沙箱,用于集成测试以验证部署。但是每种情况都不同。
–乔恩·雷诺(Jon Raynor)
17年1月9日在15:19
#5 楼
数据库集成测试还不错。甚至更多,它们是必需的。您可能将应用程序划分为多个层,这是一件好事。您可以通过模拟相邻的层来隔离地测试每个层,这也是一件好事。但是,无论您创建了多少个抽象层,在某个时候都必须有一层完成肮脏的工作-实际上是在与数据库对话。除非您进行测试,否则根本不会进行测试。如果通过模拟n-1层来测试n层,则您正在评估n层在n-1起作用的条件下的假设。为了使它起作用,您必须以某种方式证明第0层有效。
虽然理论上可以通过解析和解释生成的SQL来对数据库进行单元测试,但是创建测试的方法要容易得多且更可靠
结论
对单元测试抽象存储库,Ethereal对象关系映射器,通用活动记录,理论上的持久性有何信心?层,最后生成的SQL包含语法错误吗?
评论
我当时想添加与您类似的回复,但您说的更好!根据我的经验,在获取和存储数据的层上进行了一些测试,这使我免于痛苦。
–丹尼尔·霍林拉克(Daniel Hollinrake)
2015年11月4日14:37
数据库集成测试是不好的。您的ci / cd行中是否有可用的数据库?对我来说,这似乎很复杂。模拟数据库内容并构建一个抽象层使用它要容易得多。这不仅是一个更加优雅的解决方案,而且还尽可能快。单元测试需要快速。测试数据库会大大降低单元测试的速度,使其达到无法接受的水平。即使进行成千上万次单元测试,单元测试也不应花费超过10分钟的时间。
–大卫
18年11月12日在8:46
@David您的ci / cd行中是否有可用的数据库?当然,这是非常标准的功能。顺便说一句,我不是提倡集成测试而不是单元测试-我是提倡将集成测试与单元测试结合使用。快速的单元测试是必不可少的,但是数据库过于复杂,无法依赖具有模拟交互的单元测试。
–el.pescado
18年11月13日在8:24
@ el.pescado我必须不同意。如果您的数据库通信位于抽象层的后面,那么模拟起来真的很容易。您可以决定要返回哪个对象。同样,某些东西是标准的事实并不能使它成为一件好事。
–大卫
18年11月13日在21:26
@David我认为这取决于您如何处理数据库。它是实施细节还是系统的重要组成部分? (我倾向于后者)。如果您将数据库视为愚蠢的数据存储,那么可以,您可能不需要集成测试。但是,如果数据库中存在任何逻辑-约束,触发器,外键,事务,或者您的数据层使用自定义SQL而不是普通的ORM方法,那么我觉得仅凭单元测试是不够的。
–el.pescado
18年11月14日在6:59
#6 楼
您所引用的博客文章的作者主要关注的是集成测试可能带来的潜在复杂性(尽管它以非常有根据和明确的方式编写)。但是,集成测试不一定是坏的,而且实际上比纯单元测试更有用。这实际上取决于应用程序的上下文以及您要测试的内容。
如果数据库服务器出现故障,今天的许多应用程序根本无法工作。至少,要在要测试的功能的上下文中考虑它。
一方面,如果要测试的内容不依赖于或无法实现完全依赖数据库,然后以甚至不尝试使用数据库的方式编写测试(只需根据需要提供模拟数据)。
例如,如果您尝试在为网页提供服务时(例如)测试一些身份验证逻辑,将其与DB分开可能是一件好事(假设您不依赖DB进行身份验证,或者您可以合理地模拟它)。 br />
另一方面,如果它是直接依赖于数据库的功能,并且在数据库不可用的情况下根本无法在实际环境中工作,则可以模拟数据库在数据库客户端中的功能代码(即使用该DB的层)不一定有意义。
例如,如果您知道您的应用程序将依赖于数据库(并且可能依赖于特定的数据库系统),那么为此而对数据库行为进行模拟通常会浪费时间。数据库引擎(尤其是RDBMS)是复杂的系统。几行SQL实际上可以执行很多工作,这很难模拟(实际上,如果您的SQL查询长几行,则可能需要更多行Java / PHP / C#/ Python代码以在内部产生相同的结果):复制已经在数据库中实现的逻辑是没有意义的,然后检查测试代码本身将成为问题。
我不会一定把这当作单元测试的问题集成测试,而是看待测试的范围。
单元测试和集成测试的总体问题仍然存在:您需要一套合理可行的测试数据和测试用例集,但是对于那些测试用例来说,它也足够小要快速执行的测试。
重置数据库和重新填充测试数据的时间是要考虑的一个方面;您通常会根据编写模拟代码所花费的时间(最终也必须维护)来评估它。
要考虑的另一点是应用程序对数据库的依赖程度。
如果您的应用程序仅遵循CRUD模型,那么您在其中具有一层抽象层,可以通过简单的配置设置在任何RDBMS之间进行交换,那么您很可能会能够非常轻松地使用模拟系统(可能使用内存中的RDBMS来模糊单元测试和集成测试之间的界线)。
如果您的应用程序使用更复杂的逻辑,则某些特定于SQL Server的逻辑,MySQL,PostgreSQL(例如),那么使用该特定系统进行测试通常会更有意义。
评论
“如果数据库服务器出现故障,今天的许多应用程序根本根本无法工作”-这是非常重要的一点!
–el.pescado
2015年11月4日在14:15
很好地解释了复杂模拟的局限性,例如使用另一种语言模拟SQL。当测试代码变得足够复杂以致似乎需要对其进行自我测试时,这就是QA的味道。
–dodgethesteamroller
15年11月12日在18:49
#7 楼
两者都需要。在您的示例中,如果您要在某个条件下测试数据库,则在运行
findByKeyword
方法时,您会取回数据,您希望这是一种很好的集成测试。 > 在使用该
findByKeyword
方法的任何其他代码中,您都想控制向测试馈入的内容,因此您可以为测试返回空值或正确的单词,或者模拟数据库依赖项,等等确切知道您的测试将收到什么(并且您将失去连接数据库并确保其中数据正确的开销)#8 楼
您认为这样的单元测试不完整是正确的。不完整在于被模拟的数据库接口中。这种天真的模拟的期望或断言是不完整的。要使其变得完整,您必须花费足够的时间和资源来编写或集成SQL规则引擎,以确保被测对象发出的SQL语句能够实现预期的操作。
但是,通常被遗忘且昂贵的模拟替代品/同伴产品是“虚拟化”。
您能否启动一个临时的,内存中但“真实的”数据库实例用于测试单个功能?是的在那里,您有一个更好的测试,它可以检查保存和检索的实际数据。
现在,也许有人会说,您已经将单元测试变成了集成测试。关于在单元测试和集成测试之间划分界限的位置有不同的看法。恕我直言,“单位”是一个任意定义,应符合您的需求。
评论
这似乎只是重复几个小时前发布的先前回答中提出和解释的观点。
– gna
2015年11月3日在21:15
#9 楼
Unit Tests
和Integration Tests
相互正交。它们为您正在构建的应用程序提供了不同的视图。通常您都想要。但是,当您需要哪种测试时,时间点有所不同。 最常见的是
Unit Tests
。单元测试专注于所测试代码的一小部分-确切的称为unit
留给读者。但是目的很简单:获得代码何时何地中断的快速反馈。也就是说,应该很清楚,对实际数据库的调用是一个非逻辑操作。 另一方面,有些东西只能在没有数据库的情况下进行严格的单元测试。也许您的代码中存在竞争条件,并且对DB的调用引发了对
unique constraint
的违反,这仅在您实际使用系统时才会抛出。但是这类测试非常昂贵,您无法(也不希望)像unit tests
那样频繁地运行它们。#10 楼
在.Net世界中,我习惯于创建一个测试项目,并创建测试来作为减去UI的双向编码/调试/测试方法。这是我发展的有效途径。我对运行每个版本的所有测试都不感兴趣(因为这确实减慢了我的开发工作流程),但我知道这样做对更大的团队有用。不过,您可以制定一个规则,即在提交代码之前,应运行并通过所有测试(如果由于实际上是在命中数据库,则测试需要更长的时间才能运行)。模拟数据访问层(DAO)并没有真正访问数据库,不仅不允许我按照自己喜欢的方式进行编码,而且还错过了很大一部分实际代码库。如果您不是真正地测试数据访问层和数据库,而是假装,然后花大量时间进行模拟,那么我将无法掌握这种方法在实际测试代码中的用处。我正在测试一小块,而不是一次大的。我了解我的方法可能更像是集成测试,但是如果您实际上只写一次并第一次编写集成测试,那么带有模拟的单元测试似乎是浪费时间。这也是开发和调试的好方法。
实际上,一段时间以来,我已经了解了TDD和行为驱动设计(BDD),并思考了使用方法,但是很难做到这一点。追溯添加单元测试。也许我错了,但是编写一个包含更多端对端代码并包含数据库的测试似乎是一种更完整,优先级更高的测试,它涵盖了更多代码,并且是编写测试的更有效方法。 >
实际上,我认为应该采用诸如行为驱动设计(BDD)之类的方法来尝试使用领域特定语言(DSL)进行端到端测试。我们在.Net世界中拥有SpecFlow,但它始于Cucumber,是开放源代码。
https://cucumber.io/
我真的对我编写的模拟数据访问层而不访问数据库的测试的真正实用性印象深刻。返回的对象没有命中数据库,也没有填充数据。这是一个完全空的对象,我不得不以一种不自然的方式对其进行模拟。我只是觉得这很浪费时间。
根据堆栈溢出,当实际对象不适合合并到单元测试中时,可以使用模拟。
https:// stackoverflow .com / questions / 2665812 / what-is-mocking
“模拟主要用于单元测试。被测对象可能与其他(复杂)对象具有依赖性。要隔离该对象的行为您想测试用模拟真实对象行为的模拟替换其他对象。如果真实对象不适合合并到单元测试中,这将非常有用。“
我的观点是我正在端到端进行任何编码(从Web UI到业务层,从数据访问层到数据库,往返),在以开发人员身份签入任何内容之前,我将测试此往返流程。如果我从测试中切出UI并调试并测试此流程,那么我将测试UI之外的所有内容,并返回UI期望的结果。我剩下的就是向UI发送所需的内容。
我有一个更完整的测试,这是我自然开发工作流程的一部分。对我来说,那应该是最高优先级的测试,它涵盖了尽可能多地端对端实际用户规范的测试。如果我再也没有创建任何其他更详尽的测试,至少我还有一个更完整的测试,可以证明我所需的功能有效。
Stack Exchange的共同创始人不相信拥有100%单元测试覆盖率。我也不是。我将进行更完整的“集成测试”,该测试对数据库的影响超过了每天维护一堆数据库模拟的次数。
https://www.joelonsoftware.com/2009/01/31/from-podcast-38/
评论
很明显,您不了解单元测试和集成测试之间的区别
–BЈовић
17 Mar 10 '17 at 10:55
我认为这取决于项目。在资源较少的小型项目中,由于缺乏测试人员,开发人员更全面地负责测试和回归测试,并且使文档与代码保持同步,因此如果我要花时间编写测试,那将是给了我最大的收益。我想用一块石头杀死尽可能多的鸟。如果我的大多数逻辑和错误来自于生成报告的数据库存储过程,还是来自前端JavaScript,那么在中间层进行完整的单元测试将无济于事。
–user3198764
17年3月11日15:43
#11 楼
应该嘲笑外部依赖关系,因为您无法控制它们(它们可能在集成测试阶段通过但在生产中失败)。驱动器可能会失败,数据库连接可能由于多种原因而失败,可能会出现网络问题等。进行集成测试并不能给人任何额外的信心,因为它们都是运行时可能发生的问题。使用真实的单元测试,您可以在沙盒的范围内进行测试,并且应该清楚。如果开发人员编写的SQL查询在QA / PROD中失败,则意味着他们甚至在那之前都没有测试过一次。
评论
+1表示“您无法控制它们(它们可能在集成测试阶段通过但在生产中失败)”。
–图兰斯·科尔多瓦(TulainsCórdova)
2015年11月2日在20:24
您可以控制它们达到满意的程度。
–el.pescado
15年11月2日在21:29
我明白你的意思,但是我认为这比现在更真实了吗?使用自动化和工具(如Docker),您实际上可以准确而可靠地复制和重复所有二进制/服务器依赖项的设置,以进行集成测试套件。当然,是的,物理硬件(和第三方服务等)可能会失败。
– mindplay.dk
2015年11月2日在22:15
我绝对不同意。您应该编写(其他)集成测试,因为外部依赖项可能会失败。外部依赖项可能有其自身的怪癖,在嘲笑所有内容时您很可能会错过这些怪癖。
–Paul Kertscher
2015年11月3日,9:05
@PaulK仍在思考标记为已接受的答案,但我倾向于相同的结论。
– mindplay.dk
2015年11月3日,11:23
评论
阅读agitar.com/downloads/TheWayOfTestivus.pdf,尤其是第6页“测试比单元更重要”。@ user61852在说明中说“天真”,是吗?
您的同事如何完全确定其模拟数据库的行为与真实行为相同?
您正在尝试变得现实。您的同事正在尝试遵守规则。始终编写能够产生价值的测试。不要浪费时间编写无法维护的测试,也不要编写不执行以下操作之一的测试:增加代码正确的可能性,或者强迫您编写更具可维护性的代码。
@ mindplay.dk:该段中的关键句子是“但是不要卡在任何教条上。编写需要编写的测试。”您的同事似乎陷入了教条中,并且您不需要别人在您的示例中解释您需要编写的测试内容-您已经知道这一点。很明显,测试数据库是否理解查询,您必须对真实的数据库运行查询-没有模拟可以告诉您这一点。