【问题标题】:How can I avoid multiple asserts in this unit test?如何在此单元测试中避免多个断言?
【发布时间】:2012-01-10 00:34:08
【问题描述】:

这是我第一次尝试做单元测试,请耐心等待。
I'm still trying to unit test a library that converts lists of POCOs to ADO.Recordsets.

现在,我正在尝试编写一个创建 List<Poco> 的测试,将其转换为 Recordset(使用我想要测试的方法),然后检查它们是否包含相同的信息(例如,如果 @987654325 @,等等...)。

这是 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 页) em>:

在他的示例中,被测方法返回一个带有多个属性的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();

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

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

【问题讨论】:

  • 这就是为什么它是单元测试的“艺术”而不是“科学”...
  • 一个测试用例中的多个断言并不是邪恶的。每个测试用例应该只有一个断言is foolish的想法。

标签: c# unit-testing assert


【解决方案1】:

我认为,从本质上讲,这很好。如果我没记错的话,多个断言是“邪恶”的原因是它意味着您在一个测试中测试多个事物。在这种情况下,您确实正在这样做,因为您正在测试每个字段,大概是为了确保这适用于几种不同的类型。既然这就是对象相等性测试所能做的一切,我认为你是清楚的。

如果您真的想对此持激进态度,请为每个属性编写一个测试 (j/k!)

【讨论】:

    【解决方案2】:

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

    我会将“每个测试一个断言”归类为指导方针,而不是硬性规定。当您忽略它时,请考虑为什么您忽略它。

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

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

    【讨论】:

    • 真的很喜欢你的方法!
    【解决方案3】:

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

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

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

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

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          猜你喜欢
          • 2011-06-28
          • 1970-01-01
          • 1970-01-01
          • 2011-02-22
          • 1970-01-01
          • 2021-12-05
          • 1970-01-01
          • 1970-01-01
          • 2014-09-07
          相关资源
          最近更新 更多