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

本文关键字:断言 单元测试 | 更新日期: 2023-09-27 18:19:48

这是我第一次尝试进行单元测试,所以请耐心等待。
我仍在尝试对一个库进行单元测试,该库将POCO列表转换为ADO.Recordsets.

现在,我正在尝试编写一个测试,创建一个List<Poco>,将其转换为记录集(使用我想要测试的方法),然后检查它们是否包含相同的信息(例如,如果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的优秀著作《单元测试的艺术》就在我面前,他有一个例子正好涵盖了这一点(对于那些有这本书的人来说:第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();

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

所以。。。我现在能做什么
在这种特殊情况下,这种程度的重复仍然可以接受吗?或者有更好的方法来测试这一点吗?

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

我认为,从本质上讲,这是可以的。如果我没有记错的话,多个断言之所以是"邪恶的",是因为它意味着你在一个测试中测试多个东西。在这种情况下,您确实在测试每个字段,大概是为了确保这适用于几种不同的类型。既然这就是对象平等测试所能做的一切,我想你已经清楚了。

如果你真的想对它采取激进的态度,那么在我的书中,为每个属性(j/k!)编写一个测试

每个单元测试多个断言是完全可以的,只要多个断言都断言相同的测试条件。在您的案例中,他们正在测试转换是否成功,因此测试通过的条件是所有这些断言都为真。结果,一切都很好!

我会把"每个测试一个断言"归类为一个指导原则,而不是硬性规定。当您忽略它时,请考虑为什么您忽略它。

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

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

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

然而,在单个单元测试中拥有多个断言与为每个属性单独进行单元测试是有区别的。我称之为"阻塞断言"(可能是一个更好的名字)。如果在一个测试中有多个断言,那么您只会知道使断言失败的第一个属性中的失败。如果你有10个属性,其中5个返回了错误的结果,那么你必须修复第一个,重新运行测试并注意到另一个失败,然后修复它等等。

根据你的看法,这可能会很令人沮丧。另一方面,5个简单的单元测试突然失败也可能令人反感,但这可能会让你更清楚地了解是什么导致这些测试同时失败,并可能更快地找到已知的解决方案(也许)。

我想说的是,如果您需要测试多个属性,请将数字降低(可能低于5),以避免阻塞断言问题失控。如果有大量的属性需要测试,那么这可能是一个迹象,表明您的模型代表了太多,或者您可以考虑将属性分组为多个测试。

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

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

这应该是值得检查的http://rauchy.net/oapt/为每个断言生成一个新测试用例的工具。