接口和单元测试 - 总是白盒测试?

时间:2009-09-13 16:31:05

标签: unit-testing

我终于想到了让我担心依赖注入和类似的技术让单元测试更容易的问题。我们来看这个例子:

public interface IRepository { void Item Find(); a lot of other methods here; }
[Test]
public void Test()
{
  var repository = Mock<IRepository>();
  repository.Expect(x => x.Find());
  var service = new Service(repository);
  service.ProcessWithItem();
}

现在,上面的代码出了什么问题?这是我们的测试大致进入ProcessWithItem()实现。如果它想“从GetAll()中的x做x ......” - 但是不,我们的测试知道那里会发生什么。这只是一个简单的例子。几乎没有想象我们的测试现在与之相关,当我们想要从方法中的GetAll()更改为更好的GetAllFastWithoutStuff()时...我们的测试被打破了。请改变它们。很多糟糕的工作经常发生而没有任何实际需要。

这就是我经常停止写测试的原因。我不知道如何在不了解实现细节的情况下进行测试。而且了解它们,测试现在非常脆弱和痛苦。

当然,它不仅仅是关于接口(或DI)。 POCO(和POJO,为什么不呢)也遭受同样的困扰,但它们现在与数据联系在一起,而不是与界面联系在一起。但原理是一样的 - 我们的最终断言与我们对SUT将要做的事情的知识紧密相关。 “是的,你必须提供这个领域,先生,这更好地具有这个价值”。

因此,测试很快就会失败。这很痛苦。而这个问题。

有没有什么技巧可以解决这个问题? AutoMockingContainer(基本上关注所有ALL方法和嵌套的DI层次结构)看起来很有希望,但有其自身的缺点。还有什么吗?

5 个答案:

答案 0 :(得分:3)

依赖注入本身可以让你注入一个IRepository实现,它接受对它进行的任何调用,检查是否满足不变量和前置条件,并返回满足后置条件的结果。当你选择注入一个对将要调用哪些方法有非常具体期望的模拟对象时,是的,你正在进行高度特定于实现的测试 - 但依赖注入在这个问题上是完全无辜的,因为它从来没有规定你是什么应注射;相反,你的牛肉似乎与Mocking有关 - 事实上,特别是你选择使用的有点自动化的模拟方法,这是基于非常具体的期望。

对非常具体的期望进行模拟仅对白盒测试有用。根据你正在使用的工具/框架/库(你甚至没有在标签中指定确切的编程语言,所以我假设你的问题是完全开放的)你可以指定允许的自由度(允许这些调用以任何顺序进行,这些参数必须只满足以下前提条件等等)。但是,我不知道有一个自动化工具可以完全执行opaque-box测试所需的内容,这是“通用,容忍的实现yonder接口,所有''合同编程'检查是必需的,不需要其他”。

在项目的整个生命周期中,我倾向于为所需的主要界面建立一个“不完全模拟”的库。在某些情况下,从一开始这些可能有些明显,但在其他情况下,随着我正在考虑进行一些重大的重构,它们会逐渐出现,如下所示(典型场景)......:

重构的早期阶段打破了脆弱的强烈期望的一些方面,嘲笑我最初便宜地实施,我想是否只是调整预期或全力以赴,如果我认为它不是一次性的(即未来的重构和测试的回报将证明投资的合理性)然后我手工编写一个好的“不太嘲笑”并将其存放在项目的特定技巧包中 - 实际上通常可以在项目中重复使用;像MockFilesystem,MockBigtable,MockDom,MockHttpClient,MockHttpServer等这样的类/包进入项目无关的存储库并重新用于测试各种未来项目(实际上可以与公司内的其他团队共享,如果几个团队正在使用文件系统接口,bigtable接口,DOM,http客户端/服务器接口等,这些团队在整个团队中是统一的。)

我承认,如果你采用“模拟”来专门提及界面的“用于测试目的的虚假实现”的精确期望风格,那么使用“模拟”这个词可能稍微不合适。也许Stub,Shim,Fake,Test或其他一些前缀可能更可取(我确实倾向于使用Mock的历史原因,除非我记得特别称它为Fake之类的东西; - )。

如果我使用语言以清晰而准确的方式表达语言本身的 界面中的各种合同设计规范,我想我会得到大部分的自动工具支持这种假装/垫片/等;但是我主要用其他语言编写代码,所以我必须在这里做更多的手工操作。但我认为这是一个单独的问题。

答案 1 :(得分:1)

我读了一本优秀的书http://www.manning.com/rainsberger/。 我想提供一些从中获得的见解。 我相信一些建议可以帮助您减少测试和实现之间的耦合。

已编辑:此耦合中包含的测试断言正在测试的代码调用某些方法。调用某种方法绝不是功能需求,而是实现问题。它涉及的不是被测试的接口。

  1. 在许多情况下,测试应该是关于界面的外部行为,并且完全是黑盒子测试它们。

    作者给出了一个示例,测试类应该与要测试的类不同。起初,我确信这是错误的,因为它使测试protected和package方法变得更加困难。但他认为你应该只测试系统的外部行为,即公共方法。 非公共方法是实现细节,测试它会导致测试与实现耦合。这对我非常有见地。

      

    顺便说一下,这本书有很多关于如何设计测试的实用建议(比如JUnit测试),如果公司没有提供,我会用自己的钱买它! ; - )

  2. 本书的一个很好的其他建议是在功能级别而不是方法级别进行测试。例如,测试列表的add()方法需要受信任的size()和get()方法,但它们又需要add(),所以我们有一个循环,我们无法安全地测试。但是,在添加时全局测试列表的行为(包括所有方法)涉及同时测试这三种方法,而不是证明每种方法在隔离中是正确的,而是一起检查它们是否提供了预期的行为。 通常,当您尝试单独测试某个方法时,如果不使用其他方法,则无法编写合理的测试,因此您最终会测试实现;结果是测试与实施之间的耦合 仅测试功能,而不是方法

  3. 另外,请注意使用外部资源进行测试(数据库更常见,但存在许多其他数据库)要慢得多,需要从执行机器访问(IP,许可证等),需要启动容器,可能对同时访问敏感(数据库不能同时可靠地运行多个JUnit活动),并且还有许多其他缺点。如果您的所有测试都使用外部资源,那么您就遇到麻烦,无法一直运行所有测试,从任何机器,同时从多台机器运行等等。所以我理解(仍然从书中):

    只测试一次每个外部资源(例如数据库),在非单元测试的专用测试中,但是集成测试(尽管它仍然可以如果合适,使用相同的JUnit技术。)

    测试足够的专用测试以信任资源正在运行。然后,其他测试不应再测试它,这是浪费,他们应该信任它。

      

    请注意,当前的 Maven最佳实践提供了类似的建议(请参阅免费手册“使用Maven更好地构建”)。我相信这不是巧合:

         
        
    • 项目测试目录中的JUnits是真正的单元测试。每次你对项目做一些事情时都会运行(除了编译)。
    •   
    • 集成和功能测试应该在不同的项目中进行,即集成测试项目。在将整个应用程序部署到容器中之后,它们仅在稍后(可选)阶段运行。
    •   

答案 2 :(得分:1)

  

因此,测试将会进行   失败 - 很快也经常。这很痛苦。   而这个问题。

是的,单元测试可能取决于内部实现细节。当然,这种“白盒子”测试比“黑盒子”测试更脆弱,“黑盒子”测试只依赖于外部发布的合同。

但我不同意这会导致定期测试失败。考虑一下你是如何使用mocks进行测试的:你已经使用依赖注入来限制类的职责,减少与其他代码的耦合,并启用单独测试类

  

是否有任何技术可以处理   此?

良好的单元测试只有在您更改被测试的类时才会失败,即使它取决于内部实现细节。而且你可以限制你的班级的责任和耦合(到其他班级),这样你就很少需要改变它。

在实践中,你必须务实;你会不时地编写“单元测试”,实际上是涉及多个类或超大类的集成测试。在这种情况下,根据内部实施细节进行的脆弱测试更为危险。但对于真正的TDD风格的课程,并非如此。

答案 3 :(得分:0)

请记住,在编写测试时,如果您没有测试存储库,则需要测试Service类。在此特定示例中,ProcessWithItem方法。您创建了对存储库对象的期望。顺便说一下,您忘了为x.Find方法指定预期的回报。这就是DI的美妙之处在于你将所有内容与你要编写的代码隔离开来(我假设你做了TDD)。

老实说,我无法解释你所描述的问题。

答案 4 :(得分:0)

是的,这是单元测试的一大问题。那,和重构。并设计Agile常见的更改。而那些创造测试的人缺乏经验。等等...

我认为普通非关键系统开发人员唯一可以做的就是明智地选择你的战斗。在开发早期确定真正的关键路径并测试它们。在花费大量时间测试其余代码之前,权衡代码更改的可能性。

如果有人全力以赴,请告诉我们。