【问题标题】:Unit testing of extremely trivial methods (yes or no)极其琐碎的方法的单元测试(是或否)
【发布时间】:2010-11-16 07:59:54
【问题描述】:

假设你有一个方法:

public void Save(Entity data)
{
    this.repositoryIocInstance.EntitySave(data);
}

你会写一个单元测试吗?

public void TestSave()
{
    // arrange
    Mock<EntityRepository> repo = new Mock<EntityRepository>();
    repo.Setup(m => m.EntitySave(It.IsAny<Entity>());

    // act
    MyClass c = new MyClass(repo.Object);
    c.Save(new Entity());

    // assert
    repo.Verify(m => EntitySave(It.IsAny<Entity>()), Times.Once());
}

因为稍后如果您确实更改方法的实现以执行更“复杂”的事情,例如:

public void Save(Entity data)
{
    if (this.repositoryIocInstance.Exists(data))
    {
        this.repositoryIocInstance.Update(data);
    }
    else
    {
        this.repositoryIocInstance.Create(data);
    }
}

...您的单元测试会失败,但它可能不会破坏您的应用程序...

问题

我是否应该为没有任何返回类型*或**不更改内部模拟之外的任何内容的方法创建单元测试?

【问题讨论】:

    标签: unit-testing nunit moq


    【解决方案1】:

    问自己两个问题。 “这个单元测试的手动等效项是什么?”和“值得自动化吗?”。在你的情况下,它会是这样的:

    什么是手动等效? - 启动调试器 - 进入“保存”方法 - 进入下一步,确保您在 IRepository.EntitySave 实现中

    值得自动化吗?我的回答是“不”。从代码中可以 100% 明显看出。 从数百个类似的废物测试中,我没有看到一个有用的。

    【讨论】:

      【解决方案2】:

      您的问题的简短回答是:是的,您绝对应该测试这样的方法。

      我认为 Save 方法实际上保存数据是 重要的。如果你不为此编写单元测试,那你怎么知道?

      其他人可能会出现并删除调用 EntitySave 方法的那行代码,并且任何单元测试都不会失败。稍后,您想知道为什么项目从不持久化...

      在您的方法中,您可以说删除该行的任何人只有在他们有恶意时才会这样做,但问题是:简单的事情不一定保持简单,您最好在事情发生之前编写单元测试复杂。

      不是 Save 方法调用存储库上的 EntitySave 的实现细节 - 它是预期行为的一部分,也是非常关键的部分,如果我可以这么说的话。您要确保数据确实被保存。

      仅仅因为一个方法没有返回值并不意味着它不值得测试。一般来说,如果您观察到良好的命令/查询分离 (CQS),任何 void 方法都应该会改变 something 的状态。

      有时某些东西是类本身,但有时它可能是其他东西的状态。在这种情况下,它会更改存储库的状态,这就是您应该测试的内容。

      这称为测试Inderect Outputs,而不是更正常的Direct Outputs(返回值)。

      诀窍是编写单元测试,这样它们就不会经常中断。使用 Mocks 时,很容易意外编写 Overspecified Tests,这就是为什么大多数 Dynamic Mocks(如 Moq)默认为 Stub 模式的原因,在这种模式下如何做并不重要多次调用给定的方法。

      所有这些以及更多内容都在优秀的xUnit Test Patterns 中进行了解释。

      【讨论】:

      • 我认为您不了解单元测试和模拟。如果一个 void 方法只改变了一些其他对象的内部状态并且你为它编写了一个单元测试,你将为那个内部对象编写一个模拟。你必须写下它的行为。所以你的方法不会改变任何状态。它只会使用您预编程的模拟行为。以我在问题中解释的方式,勉强验证电话可能会有问题。这意味着测试无效,而不是单元测试测试的代码。
      • @Robert Koritnik:我认为这是第一次有人声称我不了解单元测试和测试替身......我同意如果方法 only 更改internal 状态,没有理由为它编写单元测试,但话又说回来,有这样的代码有什么理由呢?我可能已经从您的问题中得出结论,但我假设有问题的成员变量代表一个存储库,而存储库在我的书中非常代表 external 状态。
      • >> 如果你不为此编写单元测试,那你怎么知道?
      • >> Save方法实际保存数据很重要
      【解决方案3】:

      确实,您的测试取决于您的实现,这是您应该避免的事情(尽管有时并不是那么简单......)而且不一定是坏事。 但即使您的更改不会破坏代码,这些测试也会破坏

      你可以有很多方法来解决这个问题:

      • 创建一个真正进入数据库的测试并检查状态是否按预期更改(它不会不再是一个单元测试)
      • 创建一个伪造数据库并在内存中执行操作的测试对象(repositoryIocInstance 的另一种实现),并验证状态是否已按预期更改。对存储库接口的更改也会导致对该对象的更改。但是您的界面应该不会有太大变化,对吧?
      • 认为所有这些都太昂贵了,并使用您的方法,这可能会导致以后不必要地破坏测试(但是一旦机会很低,就可以冒险)

      【讨论】:

      • 到目前为止,您的回答与我想知道的最接近。但是在这种情况下,您的第二个项目符号将测试存储库而不是方法本身,不是吗?
      • 我不这么认为。这可能是一个包含 List 的对象(或者可能是一个 Set,取决于您想要什么),并且“EntitySave”只会添加实体(如果存在?我不知道它应该如何工作)。无需连接多个表或您的存储库可能会执行的其他复杂操作。
      • 如前所述,您的对象实现成本会更高,但恕我直言,这是查看调用是否以一致方式进行的最佳方式。
      • 好吧,我想这将在集成测试中涵盖,因为不会有任何模拟类。
      • 没错。集成测试会在这方面出现错误。但更难检测到错误在哪里。单元测试的目的是快速找到错误原因。此外,它们通常运行得更快(并且更频繁)。可以更快地检测和纠正重大变化。您只需决定是否物有所值;)
      【解决方案4】:

      当方法中没有断言时,您实际上是在断言不会引发异常。

      我也在努力解决如何测试 public void myMethod() 的问题。我猜如果你决定为可测试性添加一个返回值,返回值应该代表所有必要的显着事实,以了解应用程序状态发生了什么变化。

      public void myMethod()
      

      变成

       public ComplexObject myMethod() { 
       DoLotsOfSideEffects()
       return new ComplexObject { rows changed, primary key, value of each column, etc };
       }
      

      而不是

      public bool myMethod()  
        DoLotsOfSideEffects()
        return true;
      

      【讨论】:

      • 对象状态的变化最终会在外部反映出来,要么通过对象行为的不同结果,要么通过查询其知识时返回的不同结果。也正是在这些点上,对象状态变得相关。因此,测试改变内部状态的方法的方法是在它们变得可见的点测试这些行为或知识的变化。
      • @jeyoung:如果我理解正确的话,这些类型的测试超越了单元测试......它们比被测试的单元更深入,不是吗?
      • @jeyoung。你是说要测试“public void saveRow()”,你必须调用“public DataRow loadRow()”?
      • 这正是我的意思。一次测试两种方法看起来像是在抢先一步,但实际上您是在测试类的职责(保存一行并将其加载回)是否正确实现。
      【解决方案5】:

      不要忘记单元测试不仅仅是测试代码。这是关于允许您确定行为何时发生变化

      所以你可能有一些微不足道的东西。但是,您的实现会发生变化,您可能会产生副作用。您希望您的回归测试套件告诉您。

      例如人们经常说你不应该测试 setter/getter,因为它们是微不足道的。我不同意,不是因为它们是复杂的方法,而是有人可能会通过无知、胖手指的场景等不经意间改变它们。

      鉴于我刚才所说的,我肯定会为上述实现测试(通过模拟,和/或也许值得在设计您的类时考虑到可测试性并让它们报告状态等)

      【讨论】:

      • 我同意你所说的回归测试。而且我还要说集成测试也会显示一些非工作部分。但是由于我没有为我的 DAL 创建单元测试(它是生成的),我想我不应该创建单元测试,除非我将我的方法更改为具有某种返回类型(如 bool)来报告状态。
      • 我会说“回归”涵盖了单元和集成——它表明它们连续运行(比如在每次构建时)。至于测试自动生成的代码,我认为这是一个可以采取任何一种方式的决定,这取决于你对生成过程的信心,它的复杂程度等等。
      【解决方案6】:

      一般的经验法则是,您测试所有可能会破坏的东西。如果您确定该方法足够简单(并且保持足够简单)不会成为问题,那么可以通过测试解决。

      第二件事是,你应该测试方法的契约,而不是实现。如果测试在更改后失败,但不是应用程序,那么您的测试测试不正确。测试应涵盖对您的应用程序很重要的案例。这应该确保对不会破坏应用程序的方法的每次更改也不会通过测试。

      【讨论】:

      • 在这种情况下,这个方法不需要单元测试,因为不会破坏应用程序,所以测试会破坏。在这种情况下,我正在测试实现,而不是方法范围内不存在的合同。
      【解决方案7】:

      “你的单元测试会失败,但它可能不会破坏你的应用程序”

      这 - 实际上 - 知道这一点非常重要。这可能看起来很烦人且微不足道,但当其他人开始维护您的代码时,他们可能对 Save 进行了非常糟糕的更改并(不太可能)破坏了应用程序。

      诀窍是优先考虑。

      首先测试重要的东西。当事情进展缓慢时,为琐碎的事情添加测试。

      【讨论】:

      • 你可能在最后一句话中是对的(+1)。这里的主要问题是测试会失效,而不是代码会变成更好的版本。
      【解决方案8】:

      不返回任何结果的方法仍会更改应用程序的状态。在这种情况下,您的单元测试应该测试新状态是否符合预期。

      【讨论】:

      • 单元测试中的新状态完全取决于模拟......这就是为什么我有第二个想法。如果我的所有方法都是 col 到存储库中,并且在测试中对其进行了模拟,那么它将根据模拟而非现实生活情况获得可预测的结果。它总会过去的。
      • 您不应该测试模拟,而应该测试与模拟交互的操作。比如说,你有一个对象X 和一个调用存储库的方法M()。您想测试是否实际调用了存储库,因此您编写了存储库的模拟,然后调用您的 X.M() 方法。测试应该触发模拟中的更改,这将证明您的方法 M() 可以按要求工作。当您拥有存储库的实际实现时,您可以测试该实现(而不是模拟)——然后您将测试存储库。
      • @jeyoung:但这是一个单独的单元测试。这是存储库的单元测试,而不是按照上面的代码调用存储库的类......但是是的。问题是我不测试 DAL,因为它是生成的代码。并期望按应有的方式工作。而且我不打算对它进行单元测试。
      • 我明白了...单元测试仍然有效,因为可能有其他更改可能会破坏您的应用程序。
      猜你喜欢
      • 2012-06-30
      • 2011-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-19
      • 1970-01-01
      • 2011-05-06
      相关资源
      最近更新 更多