英文:
Unit testing method that uses already tested method
问题
假设我们有一个名为Handler
的类和一个名为Validator
的类。Handler
使用Validator
来验证传入的请求。
Validator
类已经进行了单元测试,以确保返回适当的错误等。
稍后,我们想为Handler
创建单元测试。Validator
已经有了单元测试,那么Handler
的测试应该如何编写?
我们是否应该编写与Validator
相同的测试,以验证Handler
是否返回从Validator
类获取的适当错误?这对我来说没有意义。
那么,在这种情况下,Handler
类的单元测试应该如何编写?
英文:
Assume we have class Handler
and class Validator
. The Handler
uses Validator
to validate incoming requests.
The Validator
class is unit tested, whether returns appropriate error etc.
Later, we want to create unit tests for Handler
. The Validator
has already unit tests, so how would the test for Handler
look?
Will we write the same tests as for Validator
whether Handler
returns the appropriate error (retrieved from Validator
class)? It doesn't make sense to me.
So how would the unit test for Handler
class look in this case?
答案1
得分: 3
Mureinik提出的嘲笑解决方案确实是标准解决方案。这是地球上大多数人做的事情,也是大多数人认为正常的事情。在我看来,这也是非常误导人的,我不是唯一一个这样认为的人:
- 在视频 Thoughtworks - TW Hangouts: Is TDD dead? (youtube) 中,21分10秒处,Kent Beck (Wikipedia) 说:“我的个人做法是几乎不使用模拟。”
- 在同一视频中,23分56秒处,Martin Fowler (Wikipedia) 补充道:“我和Kent一样,几乎不使用模拟。”
- 在他的书 xUnit Test Patterns: Refactoring Test Code (xunitpatterns.com) 的“Fragile Test”部分,作者Gerard Meszaros 表示“广泛使用模拟对象会导致测试过于耦合。”
- 在他的演讲 TDD, where did it all go wrong? (InfoQ, YouTube) 中,49分32秒处,Ian Cooper 表示:“我强烈反对使用模拟,因为它们过于具体。”
如果您想了解更多关于为什么模拟是一个不好的主意的信息,请阅读我的博客文章:michael.gr - On Mock Objects and Mocking
处理这个问题的更好方法是我称之为增量集成测试的方法。这意味着永远不要模拟任何东西,始终在测试中集成实际的依赖关系(或者它们的伪装,但永远不要模拟),只需安排测试的执行顺序,以便首先测试最依赖的类,然后测试依赖于它们的类。这样,Handler
的测试可以使用 Validator
并假定它有效,因为 Validator
的测试已经运行并通过了。
不幸的是,测试框架几乎没有提供按特定顺序执行测试的支持。我编写了一个工具,用于为基于Maven的Java项目处理此问题,但您可能不使用Java,Maven,或者不愿意使用某个某人构建的奇怪工具。幸运的是,还有一种手动解决方法:测试框架通常按字母顺序执行测试,因此您仍然可以通过以使它们的字母顺序与应该执行的顺序一致的方式来命名它们来强制执行测试的执行顺序。例如,您可以将测试命名为 T01_ValidatorTest
,T02_HandlerTest
等,以便 Validator
的测试始终在 Handler
的测试之前运行。您可能还需要类似地命名您的包和/或命名空间。
有关增量集成测试的更多信息,请参阅我的博客:michael.gr - Incremental Integration Testing
英文:
The mocking solution that Mureinik presented in his answer is indeed the textbook solution. It is what most people on the planet do, and what most people consider normal. In my opinion, it is also deeply misguided, and I am not the only one who thinks this way:
- In the video Thoughtworks - TW Hangouts: Is TDD dead? (youtube) at 21':10'' Kent Beck (Wikipedia) says "My personal practice is I mock almost nothing."
- In the same video, at 23':56'' Martin Fowler (Wikipedia) adds "I'm with Kent, I hardly ever use mocks."
- In the Fragile Test section of his book xUnit Test Patterns: Refactoring Test Code (xunitpatterns.com) author Gerard Meszaros states "extensive use of Mock Objects causes overcoupled tests."
- In his presentation TDD, where did it all go wrong? (InfoQ, YouTube) at 49':32'' Ian Cooper says "I argue quite heavily against mocks because they are overspecified."
If you would like to read more about why mocks are a bad idea, see my blog post: michael.gr - On Mock Objects and Mocking
A better way to handle this is a method that I call Incremental Integration Testing. This means never mock anything, always integrate the actual dependencies in your tests, (or fakes thereof, but never mocks,) and simply arrange the order in which your tests are executed so that the most dependent-upon classes are tested first, and classes that depend on them are tested afterwards. This way, the test for the Handler
can make use of the Validator
and take it for granted that it works, because the test for the Validator
has already run, and it has passed.
Unfortunately, testing frameworks offer very little, if any, support for executing tests in a particular order. I have written a tool that will take care of this for maven-based java projects, but you might not be using java, or maven, or you might not be willing to use some weird tool that some guy built. Luckily, there is a manual workaround: Testing frameworks tend to execute tests in alphabetic order, so you can still enforce the order of execution of your tests by naming them in such a way that their alphabetic order coincides with the order in which they should be executed. For example, you can name your tests T01_ValidatorTest
, T02_HandlerTest
, etc. so that the test for Validator
always runs before the test for Handler
. You might also have to similarly name your packages and/or namespaces.
For more information about Incremental Integration Testing see my blog: michael.gr - Incremental Integration Testing
答案2
得分: 2
你不应该在Handler
的单元测试中重新测试Validator
的逻辑。
单元测试的概念是隔离要测试的最小单元的逻辑(通常是一个类)。
解决这个问题的经典方法是模拟Validator
,然后测试Handler
是否正确地响应Validator
可能具有的不同行为。
你没有标记这个问题所涉及的编程语言,但是任何半可用的语言应该都有一些库允许模拟。例如,这是我在[tag:java]中使用[tag:mockito]和[tag:JUnit]来实现的方式:
// 省略了导入部分
@ExtendWith(MockitoExtension.class)
public class HandlerTest {
// 创建一个模拟的Validator
@Mock
private Validator validator;
private Handler handler;
// 使用上述Validator设置要测试的handler
@BeforeEach
public void setUp() {
handler = new Handler(validator);
}
@Test
void handlePassedValidation() {
// 使用“通过”的验证来模拟Validator
when(validator.isValid()).thenReturn(true);
handler.handle();
// 断言当验证通过时handler是否按预期工作
}
@Test
void handleFailedValidation() {
// 使用“失败”的验证来模拟Validator
when(validator.isValid()).thenReturn(false);
handler.handle();
// 断言当验证失败时handler是否按预期工作
}
}
英文:
You shouldn't re-test the Validator
's logic in the Handler
's unit test.
The idea of unit (as opposed to component, integration, or end-to-end tests) is to isolate the logic of the minimal unit (usually a class) you're testing.
The textbook solution for this would be to mock the Validator
and then test that the Handler
properly reacts to different behaviors the Validator
may have.
You didn't tag the question with any programming language, but any half-decent language should have some library to allow mocking. E.g., here's how I'd do it in [tag:java] using [tag:mockito] and [tag:JUnit]:
// Imports snipped for brevity's sake
@ExtendWith(MockitoExtension.class)
public class HandlerTest {
// Create a mock Validator
@Mock
private Validator validator;
private Handler handler;
// Set up the handler being tested with said validator
@BeforeEach
public void setUp() {
handler = new Handler(validator);
}
@Test
void handlePassedValidation() {
// Mock the validator with a "passing" validation
when(validator.isValid()).thenReturn(true);
handler.handle();
// Assert the handler did what it should when the validation passed
}
@Test
void handleFailedValidation() {
// Mock the validator with a "failing" validation
when(validator.isValid()).thenReturn(false);
handler.handle();
// Assert the handler did what it should when the validation failed
}
}
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论