10

假设你有一个方法:

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);
    }
}

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

问题

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

4

8 回答 8

6

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

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

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

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

于 2009-08-03T10:25:45.340 回答
4

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

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

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

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

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

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

于 2017-03-28T04:48:20.363 回答
2

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

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

于 2009-08-03T10:19:01.923 回答
1

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

于 2009-08-03T10:20:40.187 回答
1

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

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

诀窍是优先考虑。

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

于 2009-08-03T10:21:30.357 回答
1

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

我也在努力解决如何测试 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;
于 2009-08-03T11:53:17.813 回答
1

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

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

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

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

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

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

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

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

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

所有这些以及更多内容都在优秀的xUnit 测试模式中进行了解释。

于 2009-08-03T15:12:01.050 回答