极其琐碎方法的单元测试(是或否)

发布于 2024-07-29 18:01:55 字数 962 浏览 3 评论 0原文

假设您有一个方法:

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

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

问题

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

Suppose you have a method:

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

Would you write a unit test at all?

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

Because later on if you do change method's implementation to do more "complex" stuff like:

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

...your unit test would fail but it probably wouldn't break your application...

Question

Should I even bother creating unit tests on methods that don't have any return types* or **don't change anything outside of internal mock?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

伏妖词 2024-08-05 18:01:55

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

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

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

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

Don't forget that unit tests isn't just about testing code. It's about allowing you to determine when behaviour changes.

So you may have something that's trivial. However, your implementation changes and you may have a side effect. You want your regression test suite to tell you.

e.g. Often people say you shouldn't test setters/getters since they're trivial. I disagree, not because they're complicated methods, but someone may inadvertently change them through ignorance, fat-finger scenarios etc.

Given all that I've just said, I would definitely implement tests for the above (via mocking, and/or perhaps it's worth designing your classes with testability in mind and having them report status etc.)

五里雾 2024-08-05 18:01:55

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

您可以有很多方法来实现这一点:

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

It's true your test is depending on your implementation, which is something you should avoid (though it is not really that simple sometimes...) and is not necessarily bad. But these kind of tests are expected to break even if your change doesn't break the code.

You could have many approaches to this:

  • Create a test that really goes to the database and check if the state was changed as expected (it won't be a unit test anymore)
  • Create a test object that fakes a database and do operations in-memory (another implementation for your repositoryIocInstance), and verify the state was changed as expected. Changes to the repository interface would incurr in changes to this object as well. But your interfaces shouldn't be changing much, right?
  • See all of this as too expensive, and use your approach, which may incur on unnecessarily breaking tests later (but once the chance is low, it is ok to take the risk)
你又不是我 2024-08-05 18:01:55

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

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

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

Ask yourself two questions. "What is the manual equivalent of this unit test?" and "is it worth automating?". In your case it would be something like:

What is manual equivalent?
- start debugger
- step into "Save" method
- step into next, make sure you're inside IRepository.EntitySave implementation

Is it worth automating? My answer is "no". It is 100% obvious from the code.
From hundreds of similar waste tests I didn't see a single which would turn out to be useful.

≈。彩虹 2024-08-05 18:01:55

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

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

The general rule of thumb is, that you test all things, that could probably break. If you are sure, that the method is simple enough (and stays simple enough) to not be a problem, that let it out with testing.

The second thing is, you should test the contract of the method, not the implementation. If the test fails after a change, but not the application, then your test tests not the right thing. The test should cover cases that are important for your application. This should ensure, that every change to the method that doesn't break the application also don't fail the test.

热情消退 2024-08-05 18:01:55

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

A method that does not return any result still changes the state of your application. Your unit test, in this case, should be testing whether the new state is as intended.

最终幸福 2024-08-05 18:01:55

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

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

诀窍是确定优先顺序。

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

"your unit test would fail but it probably wouldn't break your application"

This is -- actually -- really important to know. It may seem annoying and trivial, but when someone else starts maintaining your code, they may have made a really bad change to Save and (improbably) broken the application.

The trick is to prioritize.

Test the important stuff first. When things are slow, add tests for trivial stuff.

表情可笑 2024-08-05 18:01:55

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

我还在努力解决如何测试 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;

When there isn't an assertion in a method, you are essentially asserting that exceptions aren't thrown.

I'm also struggling with the question of how to test public void myMethod(). I guess if you do decide to add a return value for testability, the return value should represent all salient facts necessary to see what changed about the state of the application.

public void myMethod()

becomes

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

and not

public bool myMethod()  
  DoLotsOfSideEffects()
  return true;
那片花海 2024-08-05 18:01:55

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

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

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

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

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

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

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

这称为测试间接输出,而不是更正常的直接输出(返回值)。

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

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

The short answer to your question is: Yes, you should definitely test methods like that.

I assume that it is important that the Save method actually saves the data. If you don't write a unit test for this, then how do you know?

Someone else may come along and remove that line of code that invokes the EntitySave method, and none of the unit tests will fail. Later on, you are wondering why items are never persisted...

In your method, you could say that anyone deleting that line would only be doing so if they have malign intentions, but the thing is: Simple things don't necessarily stay simple, and you better write the unit tests before things get complicated.

It is not an implementation detail that the Save method invokes EntitySave on the Repository - it is part of the expected behavior, and a pretty crucial part, if I may say so. You want to make sure that data is actually being saved.

Just because a method does not return a value doesn't mean that it isn't worth testing. In general, if you observe good Command/Query Separation (CQS), any void method should be expected to change the state of something.

Sometimes that something is the class itself, but other times, it may be the state of something else. In this case, it changes the state of the Repository, and that is what you should be testing.

This is called testing Inderect Outputs, instead of the more normal Direct Outputs (return values).

The trick is to write unit tests so that they don't break too often. When using Mocks, it is easy to accidentally write Overspecified Tests, which is why most Dynamic Mocks (like Moq) defaults to Stub mode, where it doesn't really matter how many times you invoke a given method.

All this, and much more, is explained in the excellent xUnit Test Patterns.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文