【发布时间】:2011-06-28 20:48:46
【问题描述】:
我有一个从请求对象填充的 DTO,并且请求对象有很多字段。我想编写一个测试来检查 populateDTO() 方法是否将值放在正确的位置。如果我遵循每个测试一个断言的规则,我将不得不编写大量测试来测试每个字段。另一种方法是在单个测试中编写多个断言。是否真的建议每个测试规则遵循一个断言,或者我们可以在这些情况下放松。我该如何解决这个问题?
【问题讨论】:
标签: unit-testing junit
我有一个从请求对象填充的 DTO,并且请求对象有很多字段。我想编写一个测试来检查 populateDTO() 方法是否将值放在正确的位置。如果我遵循每个测试一个断言的规则,我将不得不编写大量测试来测试每个字段。另一种方法是在单个测试中编写多个断言。是否真的建议每个测试规则遵循一个断言,或者我们可以在这些情况下放松。我该如何解决这个问题?
【问题讨论】:
标签: unit-testing junit
将它们分开。单元测试应该告诉你哪个单元失败了。将它们分开还可以让您快速隔离问题,而无需经历漫长的调试周期。
【讨论】:
真的推荐only one assert per unit test吗?是的,有人提出这个建议。他们是对的吗?我不这么认为。我很难相信这些人实际上已经在真正的代码上工作了很长时间。
所以,想象一下你有一个想要进行单元测试的 mutator 方法。 mutator 有某种效果,或者你想要检查的效果。通常,mutator 的预期效果数量很少,因为许多效果表明 mutator 的设计过于复杂。每个效果一个断言,每个断言一个测试用例,每个 mutator 不需要很多测试用例,所以这个建议看起来还不错。
但这种推理的缺陷在于,这些测试只关注 mutator 的预期效果。但是,如果 mutator 中有错误,它可能会产生意想不到的错误副作用。测试做出了一个愚蠢的假设,即代码没有一整类错误,并且未来的重构不会引入此类错误。最初编写该方法时,作者可能很明显不可能产生特定的副作用,但重构和添加新功能可能会使这种副作用成为可能。
测试长寿命代码的唯一安全方法是检查突变体没有意外的副作用。但是你怎么能测试这些呢?大多数类都有一些不变量:任何mutator都无法改变的东西。例如,容器的size 方法永远不会返回负值。实际上,每个不变量都是每个 mutator(以及构造函数)的后置条件。每个 mutator 通常还具有一组不变量,用于描述它不会进行哪些更改。例如,sort 方法不会更改容器的长度。类和 mutator 不变量实际上是 every mutator 调用的后置条件。为所有它们添加断言是检查意外副作用的唯一方法。
那么,只是添加更多测试用例?在实践中,不变量的数量乘以要测试的变异器的数量很大,因此每个测试一个断言会导致许多测试用例。并且关于您的不变量的信息分散在许多测试用例中。调整一个不变量的设计更改将需要更改许多测试用例。它变得不切实际。最好为 mutator 提供参数化的测试用例,它使用多个断言检查 mutator 的多个不变量。
JUnit5 的作者似乎也同意这一点。他们提供了一个assertAll 来检查一个测试用例中的多个断言。
【讨论】:
这个结构可以帮助你拥有 1 个大断言(里面有小断言)
import static org.junit.jupiter.api.Assertions.assertAll;
assertAll(
() -> assertThat(actual1, is(expected)),
() -> assertThat(actual2, is(expected))
);
【讨论】:
您可以有一个parameterized test,其中第一个参数是属性名称,第二个参数是预期值。
【讨论】:
该规则是否扩展到循环中?考虑一下这个
Collection expectedValues = // populate expected values
populateDTO();
for(DTO dto : myDtoContainer)
assert_equal(dto, expectedValues.get(someIndexRelatedToDto))
现在我对确切的语法不是很关心,但这只是我正在研究的概念。
编辑: 在cmets之后...
答案是……不!
该原则存在的原因是您可以识别对象的哪些部分失败。如果你在一种方法中使用它们,你只会遇到一个断言,然后是下一个,然后是下一个,你不会看到所有的。
所以你可以有两种方法之一:
这取决于你,两者都有起起落落。 3. 列表项
【讨论】:
assert(dto.equals(mockObject),这就是你的意思吗?您想在不违反 1-per 原则的情况下断言每个单独的元素吗?
[警告:我在 Java/JUnit 中非常“不流利”,因此请注意以下详细信息中的错误]
有几种方法可以做到这一点:
1) 在同一个测试中编写多个断言。如果您只测试一次 DTO 生成,这应该没问题。您可以从这里开始,当这开始受到伤害时,您可以转向另一个解决方案。
2) 编写辅助断言,例如assertDtoFieldsEqual,传入预期的和实际的 DTO。在辅助断言中,您分别断言每个字段。这至少会让您产生每次测试只有一个断言的错觉,并且如果您针对多种场景测试 DTO 生成,事情就会变得更加清晰。
3) 为检查每个属性的对象实现 equals 并实现 toString 以便您至少可以手动检查断言结果以找出不正确的部分。
4) 对于每个生成 DTO 的场景,创建一个单独的测试夹具,用于生成 DTO 并在 setUp 方法中初始化预期的属性。创建一个单独的测试来测试每个属性。这也导致了很多测试,但它们至少是单行的。伪代码示例:
public class WithDtoGeneratedFromXxx : TestFixture
{
DTO dto = null;
public void setUp()
{
dto = GenerateDtoFromXxx();
expectedProp1 = "";
...
}
void testProp1IsGeneratedCorrectly()
{
assertEqual(expectedProp1, dto.prop1);
}
...
}
如果您需要在不同场景下测试 DTO 生成并选择最后一种方法,那么编写所有这些测试很快就会变得乏味。如果是这种情况,您可以实现一个抽象的基本夹具,省略有关如何创建 DTO 和设置派生类的预期属性的详细信息。伪代码:
abstract class AbstractDtoTest : TestFixture
{
DTO dto;
SomeType expectedProp1;
abstract DTO createDto();
abstract SomeType getExpectedProp1();
void setUp()
{
dto = createDto();
...
}
void testProp1IsGeneratedCorrectly()
{
assertEqual(getExpectedProp1(), dto.prop1);
}
...
}
class WithDtoGeneratedFromXxx : AbstractDtoTest
{
DTO createDto() { return GenerateDtoFromXxx(); }
abstract SomeType getExpectedProp1() { return new SomeType(); }
...
}
【讨论】:
或者你可以做一些解决方法。
import junit.framework.Assert;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class NewEmptyJUnitTest {
public NewEmptyJUnitTest() {
}
@BeforeClass
public static void setUpClass() throws Exception {
}
@AfterClass
public static void tearDownClass() throws Exception {
}
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Test
public void checkMultipleValues() {
String errMessages = new String();
try{
this.checkProperty1("someActualResult", "someExpectedResult");
} catch (Exception e){
errMessages += e.getMessage();
}
try{
this.checkProperty2("someActualResult", "someExpectedResult");
} catch (Exception e){
errMessages += e.getMessage();
}
try{
this.checkProperty3("someActualResult", "someExpectedResult");
} catch (Exception e){
errMessages += e.getMessage();
}
Assert.assertTrue(errMessages, errMessages.isEmpty());
}
private boolean checkProperty1(String propertyValue, String expectedvalue) throws Exception{
if(propertyValue == expectedvalue){
return true;
}else {
throw new Exception("Property1 has value: " + propertyValue + ", expected: " + expectedvalue);
}
}
private boolean checkProperty2(String propertyValue, String expectedvalue) throws Exception{
if(propertyValue == expectedvalue){
return true;
}else {
throw new Exception("Property2 has value: " + propertyValue + ", expected: " + expectedvalue);
}
}
private boolean checkProperty3(String propertyValue, String expectedvalue) throws Exception{
if(propertyValue == expectedvalue){
return true;
}else {
throw new Exception("Property3 has value: " + propertyValue + ", expected: " + expectedvalue);
}
}
}
也许不是最好的方法,如果过度使用可能会造成混淆......但这是可能的。
【讨论】: