2012-01-10 65 views
10

这是我第一次做单元测试,所以请耐心等待。
I'm still trying to unit test a library that converts lists of POCOs to ADO.Recordsets如何在本单元测试中避免多次断言?

现在,我试图编写一个测试,创建一个List<Poco>,将它转换成一个Recordset(使用我想测试的方法),然后检查它们是否包含相同的信息(如Poco.Foo == RS.Foo和等等......)。

这是POCO:

public class TestPoco 
{ 
    public string StringValue { get; set; } 
    public int Int32Value { get; set; } 
    public bool BoolValue { get; set; } 
} 

...这是测试至今(我使用xUnit.net):

[Fact] 
public void TheTest() 
{ 
    var input = new List<TestPoco>(); 
    input.Add(new TestPoco { BoolValue = true, Int32Value = 1, StringValue = "foo" }); 

    var actual = input.ToRecordset(); 

    Assert.Equal(actual.BoolValue, true); 
    Assert.Equal(actual.Int32Value, 1); 
    Assert.Equal(actual.StringValue, "foo"); 
} 

我做什么不喜欢这是最后的三个断言,每个POCO的一个属性。
我读过很多次,在一次测试中多重断言是邪恶的(我理解为什么,我同意)。

问题是,我该如何摆脱它们?

我有罗伊Osherove的优秀图书"The Art of Unit Testing"就在我的面前,他有这正好覆盖这(对于那些谁拥有这本书:章7.2.6,202/203页)为例

在他的示例中,被测方法返回一个带有多个属性的对象AnalyzedOutput,并且他想要声明所有属性以检查每个属性是否包含期望值。

在这种情况下的解决方案:
创建另一个AnalyzedOutput例如,与预期值填充它,并断言如果它等于被测该方法返回一个(并且覆盖Equals()才能够做到这一点)。

但我认为我不能这样做在我的情况下,因为我想测试的方法返回ADODB.Recordset

而且为了创造另一个Recordset与预期值,我首先就需要完全从头开始创建:

// this probably doesn't actually compile, the actual conversion method 
// doesn't exist yet and this is just to show the idea 

var expected = new ADODB.RecordsetClass(); 
expected.Fields.Append("BoolValue", ADODB.DataTypeEnum.adBoolean); 
expected.Fields.Append("Int32Value", ADODB.DataTypeEnum.adInteger); 
expected.Fields.Append("StringValue", ADODB.DataTypeEnum.adVarWChar); 

expected.AddNew(); 
expected.BoolValue = true; 
expected.Int32Value = 1; 
expected.StringValue = "foo"; 
expected.Update(); 

我不喜欢这样,因为这是一些基本的复制的实际转换方法(被测方法)中的代码,这是在测试中要避免的另一件事。

那么...我现在可以做什么?
在这种特殊情况下,这种重复级别仍然可以接受吗?还是有更好的方法来测试?

+8

这就是为什么它是单元测试的“艺术”而不是“科学”...... – Berryl 2012-01-10 00:54:08

+0

在一个测试案例中的多个断言并不是邪恶的。每个测试用例应该只有一个断言的想法[很愚蠢](http://stackoverflow.com/a/20300843/545127)。 – Raedwald 2016-04-05 11:37:56

回答

8

我认为,在事情的精神,这是好的。如果我记得正确的话,多重断言是“邪恶”的原因是它暗示着你在一次测试中测试了多件事情。在这种情况下,你确实正在这样做,因为你正在测试每个字段,可能是为了确保它适用于多种不同的类型。因为无论如何这都是对象平等测试所能做到的,所以我认为你很清楚。

如果你真的想成为好战的它,每写一个属性测试(J/K!)

6

只要多个断言都声明相同的测试条件,在我的书中,每个单元测试的多个断言都非常好。就你而言,他们正在测试转换是否成功,所以测试传递的条件是所有这些断言都是正确的。因此,它非常好!

我将“每个测试的一个断言”作为指导,而不是一条硬性规则。当你无视它时,考虑为什么你不理睬它。

也就是说,解决这个问题的方法是创建一个单独的测试类,在类设置中运行您的测试过程。然后每个测试只是一个属性的断言。例如:

public class ClassWithProperities 
{ 
    public string Foo { get; set; } 
    public int Bar { get; set; } 
} 

public static class Converter 
{ 
    public static ClassWithProperities Convert(string foo, int bar) 
    { 
     return new ClassWithProperities {Foo=foo, Bar=bar}; 
    } 
} 
[TestClass] 
public class PropertyTestsWhenFooIsTestAndBarIsOne 
{ 
    private static ClassWithProperities classWithProperties; 

    [ClassInitialize] 
    public static void ClassInit(TestContext testContext) 
    { 
     //Arrange 
     string foo = "test"; 
     int bar = 1; 
     //Act 
     classWithProperties = Converter.Convert(foo, bar); 
     //Assert 
    } 

    [TestMethod] 
    public void AssertFooIsTest() 
    { 
     Assert.AreEqual("test", classWithProperties.Foo); 
    } 

    [TestMethod] 
    public void AssertBarIsOne() 
    { 
     Assert.AreEqual(1, classWithProperties.Bar); 
    } 
} 

[TestClass] 
public class PropertyTestsWhenFooIsXyzAndBarIsTwoThousand 
{ 
    private static ClassWithProperities classWithProperties; 

    [ClassInitialize] 
    public static void ClassInit(TestContext testContext) 
    { 
     //Arrange 
     string foo = "Xyz"; 
     int bar = 2000; 
     //Act 
     classWithProperties = Converter.Convert(foo, bar); 
     //Assert 
    } 

    [TestMethod] 
    public void AssertFooIsXyz() 
    { 
     Assert.AreEqual("Xyz", classWithProperties.Foo); 
    } 

    [TestMethod] 
    public void AssertBarIsTwoThousand() 
    { 
     Assert.AreEqual(2000, classWithProperties.Bar); 
    } 
} 
+0

真的很喜欢你的方法! – TuomasK 2012-12-19 08:21:12

2

那些3断言是有效的。如果你使用的框架更像mspec,它看起来像:

public class When_converting_a_TestPoco_to_Recordset 
{ 
    protected static List<TestPoco> inputs; 
    protected static Recordset actual; 

    Establish context =() => inputs = new List<TestPoco> { new TestPoco { /* set values */ } }; 

    Because of =() => actual = input.ToRecordset(); 

    It should_have_copied_the_bool_value =() => actual.BoolValue.ShouldBeTrue(); 
    It should_have_copied_the_int_value =() => actual.Int32Value.ShouldBe (1); 
    It should_have_copied_the_String_value =() => actual.StringValue.ShouldBe ("foo"); 
} 

我一般使用mspec作为基准,看看我的测试是否有意义。你的测试用mspec读取得很好,这给我一些半自动化的温暖模糊,我正在测试正确的东西。

对于这个问题,你用多个断言做得更好。我讨厌看到看起来像这样的测试:

Assert.That (actual.BoolValue == true && actual.Int32Value == 1 && actual.StringValue == "foo"); 

因为当失败时,错误信息“预期真,假”是完全没有价值的。多重断言,并尽可能地使用单元测试框架,将会对你有很大的帮助。

3

我同意所有其他意见,如果你在逻辑上测试一件事情,那么这样做很好。

然而,在单个单元测试中有多个断言之间存在差异,而不是对每个属性单独进行单元测试。我称之为'阻止断言'(可能是一个更好的名字)。如果你在一次测试中有很多断言,那么你只会知道失败的第一个属性失败。如果你说有10个属性,其中5个返回不正确的结果,那么你将不得不经过修复第一个,重新运行测试,并注意到另一个失败,然后修复等。

根据你的外观在这可能是相当令人沮丧的。另一方面,有5个简单的单元测试突然失败也可能会失效,但它可能会让你更清楚地了解导致这些失败的原因,并且可能会让你更快地转向已知的修复(可能)。

我会说,如果你需要测试多个属性保持数量下降(可能在5以下),以避免阻塞断言问题失控。如果有大量属性需要测试,那么这可能表明您的模型代表了太多,或者您可能会将属性分组为多个测试。

相关问题