如何在单元测试中避免多个断言?

时间:2012-01-10 00:34:08

标签: c# unit-testing assert

这是我第一次尝试进行单元测试,所以请耐心等待 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的每个属性一个 我已多次读过一次测试中的多个断言是邪恶的(我理解其中的原因,并且我同意)。

问题是,我该怎样摆脱它们?

我有Roy 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();

我也不喜欢这个,因为这基本上是实际转换方法(测试中的方法)中某些代码的重复,这是测试中要避免的另一件事。

那么......我现在该怎么办? 在这种特殊情况下,这种重复级别是否仍然可以接受,或者有更好的方法来测试它?

5 个答案:

答案 0 :(得分:9)

我认为,根据事物的精神,这很好。如果我没记错的话,多个断言是“邪恶的”的原因是它暗示你在一次测试中测试多个东西。在这种情况下,您确实这样做是因为您正在测试每个字段,可能是为了确保它适用于多种不同类型。因为无论如何都是对象平等测试,我认为你是明确的。

如果你真的想激动它,每个属性写一个测试(j / k!)

答案 1 :(得分:7)

每个单元测试的多个断言在我的书中完全没问题,只要多个断言都断言相同的测试条件。在您的情况下,他们正在测试转换是否成功,因此测试传递是以所有这些断言为真为条件的。结果,它完全没问题!

我将“每个测试的一个断言”分类为指导,而不是硬性规则。当你忽略它时,请考虑为什么你忽视它。

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

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

答案 2 :(得分:3)

这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");

因为当失败时,错误消息“expect True,got False”完全没有价值。多个断言,并尽可能使用单元测试框架,将为您提供很多帮助。

答案 3 :(得分:3)

我同意所有其他评论,如果您在逻辑上测试一件事,那么这样做是可以的。

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

根据你的看法,这可能非常令人沮丧。另一方面,有5个简单的单元测试,突然失败也可以放弃,但它可以让你更清楚地了解导致这些失败的原因,并可能更快地引导你进入已知的修复(也许)。

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

答案 4 :(得分:1)

这应该是结帐http://rauchy.net/oapt/ 为每个断言生成新测试用例的工具。