我在过去几年中学到的一件事是单元测试被认为是“已解决的问题”。 所有信息,书籍和工具都在那里,只需拿起NUnit,您就可以开始了,对吗?
不完全是。
甚至在决定开始单元测试之前,我们都需要仔细筛查他人的实际经验。 好与坏,恐怖的故事和奇迹(“这个测试节省了我一个星期的工作!”)。 然后,我们冒险尝试,意识到:有很多东西要学习!
我想带您去单元测试的土地上。 多年来,我们Typemock的团队一直在探索这一领域,我们的探索无疑影响了我们的产品开发。 我们的主要产品Isolator最初是一个模拟框架。 但是,随着我们更多地了解人们在单元测试中遇到的现实问题,我们已经开发出了许多缓解功能。 而且,我可以告诉您,我们还有很多方面需要解决。
但是,让我们从头开始。 在Typemock,我们有一个简单的愿景:每个人都可以轻松进行单元测试。
很简单,是的。 容易实现? 好...
单元测试并不容易。 单元测试的好处是巨大的,人们已经认识到它们。 但是您需要努力工作才能获得这些好处。
我们大多数人已经拥有正在使用的代码库。 我们中的一些人很幸运能够从事Greenfield项目,但是我们大多数人都拥有称为“旧版代码”的自然元素。 当我们决定编写测试时,就是针对该代码的。 事实证明,这并不容易。
当Typemock启动时,如果不更改代码以使其适合测试,就无法为遗留代码编写测试。 那是Isolator的主要目标:提供编写单元测试而无需更改代码的能力。 借助Isolator能够模拟每种.NET类型的功能,现在可以为旧代码编写单元测试了。
不断发展的API
随着时间的推移,我们学到的一件事是要注意我们的API使用情况。 以第一组API为例。 它们基于字符串。 例如,要伪造DateTime .Now
,这是您必须要做的:
Mock mockDateTime = MockManager .MockAll< DateTime >();
mockDateTime.ExpectGetAlways( "Now" , new DateTime (2000, 1, 1));
不太好,但是可以。 但是,我们知道这些东西在重构时很容易破坏。 因此,我们转到了记录重放模型。 这是重构友好的,尽管有点尴尬:
using ( RecordExpectations recorder = RecorderManager .StartRecording())
{
DateTime .Now = new DateTime( 2000, 1, 1);
}
这行得通,并且确实被认为是编写测试的革命性方式。 但是记录重放风格已经过时了。 这个版本有一些我们要解决的技术问题。 因此,当出现lambda表达式时,API在可读性和重构方面又发生了变化:
Isolate .WhenCalled(() => DateTime .Now).WillReturn( new DateTime (2000, 1, 1));
设置了当前的API后,我们决定进行另一个简化。 我们删除了“模拟”词汇,并将其替换为“假”实例。 “模拟”和“存根”已经是超载术语,并且一直被滥用和困惑。 我们没有尝试教所有初学者细微差别,而是决定完全避开这个问题。
好邻居
作为Visual Studio插件的隔离器并不是唯一的播放器-我们必须与其他工具和提供程序融洽相处。 您可以命名为代码覆盖率工具,性能分析器,构建引擎。 隔离器需要与其他人一起玩,以便人们可以使用不同配置的不同工具轻松地运行他们的测试。
说到运行测试,在Visual Studio之外运行又如何呢? 当您开始在团队中构建自动化时,您将学到很多有关MS-Verse中各种工具的知识,包括全能的TFS。 Isolator的探查器技术需要大量的集成工作才能使测试作为连续集成过程的一部分运行。 由于不同的团队使用不同的工具集和CI服务器,因此我们需要容纳一些很适合他们工作方式的东西。
强大的API
询问甚至正在考虑开始单元测试的任何人,她会告诉您:我知道我的代码会更改,并且我不想一直修正我的测试。 你能做些什么吗?
模拟框架(与Spiderman相似)具有超能力,这是巨大的责任。 改变行为的能力来自于了解对象内部的内容。 这种X射线视觉也是其致命弱点-更改内部代码也会影响测试。
单元测试也与维护有关。 在设计API时,我们总是从这个角度考虑。 这是一个例子。 考虑对象的此构造函数(来自名为ERPStore的开源项目):
public AnonymousCheckoutController(
ISalesService salesService
, ICartService cartService
, IAccountService accountService
, IEmailerService emailerService
, IDocumentService documentService
, ICacheService cacheService
, IAddressService addressService
, CryptoService cryptoService
, IIncentiveService IncentiveService)
它需要许多接口作为输入。 在测试中,我可以伪造这些依赖关系,如下所示:
var fakeSalesService = Isolate .Fake.Instance< SalesController >();
var fakeCartService = Isolate .Fake.Instance<ICartService>();
var fakeAccountService = Isolate .Fake.Instance<IAccountService>();
var fakeEmailerService = Isolate .Fake.Instance<IEmailerService>();
var fakeDocumentService= Isolate .Fake.Instance<IDocumentService>();
var fakeCacheService = Isolate .Fake.Instance<ICacheService>();
var fakeAddressService = Isolate .Fake.Instance<IAddressService>();
var fakeCryptoService = Isolate .Fake.Instance< CryptoService >();
var fakeIncentiveService = Isolate.Fake.Instance< IncentiveService >();
var controller = new AnonymousCheckoutController (
fakeSalesService,
fakeCartService,
fakeAccountService,
fakeEmailerService,
fakeDocumentService,
fakeCacheService,
fakeAddressService,
fakeCryptoService,
fakeIncentiveService);
如果我们需要构造函数采用其他类型,该怎么办? 还是删除一个论点? 我需要更改测试。
因此,我们提出了此API,该API将构造函数的签名与测试中的用法分离开:
var controller = Isolate .Fake.Dependencies< AnonymousCheckoutController >();
就这样。 Fake.Dependencies
API创建一个类型为AnonymousCheckoutController
的真实对象,并传递虚假的依赖实现,而无需提及它们的类型。 如果构造函数发生更改,则测试仍将起作用。 我们减少了测试和代码之间的耦合,并使它更具可读性。
更好的测试
有单元测试经验的人都知道这是一项后天的技能。 我们可以学习如何编写更好的测试,但是我们通常会学习困难的方法。 我们考虑过让体验更轻松。 我们如何才能帮助人们不要犯我们的错误?
现在是时候让另一部分加入隔离器了。 它可以检查测试并标记Visual Studio内部的常见错误(例如,没有断言的测试)。 它提出了一种更好的方法,并为您提供了修复它的机会。
(点击图片放大)
改善反馈周期
长期以来,Isolator都没有测试人员。 这是我们说用户选择最佳工具的方式,我们将适应这种选择。 当我们开始解决新问题时,我们开始考虑不断发展的过程。
编写大型测试套件的经验丰富的人开始要求提高速度。 随着时间的推移,我们使隔离器的工作速度加快了,但是我们认为这并不是完整的答案。 大型测试套件需要花费一些时间才能运行,但您不必一直运行它们。 实际上,在开发会话中,仅应运行与您更改的代码相关的测试。 其余时间可以再次运行,例如在签入之前或在服务器上。
但这不是全部。 经验丰富的测试人员会查看他们三年前编写的测试,并且不敢相信他们编写了如此糟糕的测试。 糟糕的测试不仅容易打破。 有时它们不是单元测试,而是变相的集成测试。 因此,它们无法快速运行。 大型测试套件不会仅仅因为其大小而运行缓慢-它还包括本质上较慢的测试。
编写专业跑步者的最终推动力来自一个完全不同的领域:修复错误。 如果测试失败,则开始寻找导致失败的最新更改。 您试图解决脑海中的难题:我最后一次去哪儿了? 我改变了什么? 为什么此测试失败,但其他方案仍然通过? 通常,经过大量调试后,您便可以解决问题。
与世界其他地方一样,Typemock也不喜欢调试。 这就是我们的顿悟所在:一切都紧密相连。 我们正在寻求围绕开发测试经验的更快解决方案。 这不仅仅是关于编写更快的测试或更快地运行它们。 它涉及编写测试,运行测试,修复失败的测试,重复执行的完整迭代过程。
隔离器的测试运行程序试图回答所有问题。 它会自动运行相关的测试,即执行您所触摸的代码的测试。 为了获得最快的反馈,它会自动过滤掉长时间运行的测试。 它在代码上显示了涵盖的内容以及进行哪些测试。 它考虑到了最近的更改发生的位置,并且可以为您指出错误的插入方向。 并且它鼓励您在测试中涵盖更多的代码,同时通过相关的反馈使周期紧密,从而使继续操作变得容易。
摘要
到目前为止,这就是隔离器的故事。 一开始,我们想解决一个问题。 随着越来越多的人对他们的代码进行单元测试,我们了解到我们可以通过查看他们面临的挑战来为他们提供更多帮助。
单元测试仍然不容易。 我们还没有完成。
关于作者
是Typemock的产品经理。 Gil在软件开发方面拥有超过15年的经验,从编码到团队管理以及流程的实施,他在软件开发的各个方面进行了工作。 Gil进行有关单元测试的演讲,博客和讨论,并鼓励从初学者到经验丰富的开发人员在他们的项目中实施单元测试作为核心实践。 可以通过gilz@typemock.com与他的博客联系 。
翻译自: https://www.infoq.com/articles/Typemock/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1