简体   繁体   中英

How can I avoid multiple asserts in this unit test?

This is my first attempt to do unit tests, so please be patient with me.
I'm still trying to unit test a library that converts lists of POCOs to ADO.Recordsets .

Right now, I'm trying to write a test that creates a List<Poco> , converts it into a Recordset (using the method I want to test) and then checks if they contain the same information (like, if Poco.Foo == RS.Foo , and so on...).

This is the POCO:

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

...and this is the test so far (I'm using 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");
}

What I don't like about this are the three asserts at the end, one per property of the POCO.
I've read lots of times that multiple asserts in one test are evil (and I understand the reasons why, and I agree).

The problem is, how can I get rid of them?

I have Roy Osherove's excellent book "The Art of Unit Testing" right in front of me, and he has an example which covers exactly this (for those who have the book: chapter 7.2.6, page 202/203) :

In his example, the method under test returns an AnalyzedOutput object with several properties, and he wants to assert all the properties to check if each one contains the expected value.

The solution in this case:
Create another AnalyzedOutput instance, fill it with the expected values and assert if it's equal to the one returned by the method under test (and override Equals() to be able to do this).

But I think I can't do this in my case, because the method that I want to test returns an ADODB.Recordset .

And in order to create another Recordset with the expected values, I would first need to create it completely from scratch:

// 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();

I don't like this either, because this is basically a duplication of some of the code in the actual conversion method (the method under test), which is another thing to avoid in tests.

So...what can I do now?
Is this level of duplication still acceptable in this special situation, or is there a better way how to test this?

I'd argue that in the spirit of the thing, this is fine. The reason that multiple asserts are "evil", if I recall correctly, is that it implies that you are testing multiple things in one test. In this case, you are indeed doing that in that you are testing each field, presumably to make sure this works for several different types. Since that's all an object equality test would do anyway, I think you are in the clear.

If you really wanted to be militant about it, write one test per property (j/k!)

Multiple assertions per unit test are perfectly fine in my book, as long as the multiple assertions are all asserting the same test condition. In your case, they're testing that the conversion was successful, so the test passing is conditional on all of those assertions being true. As a result, it's perfectly fine!

I'd classify "one assertion per test" as a guideline, not a hard-and-fast rule. When you disregard it, consider why you're disregarding it.

That said, a way around it is to create a single test class that, on class setup, runs your test process. Then each test is just an assertion on a single property. For example:

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

Those 3 asserts are valid. If you used a framework more like mspec, it would look like:

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

I generally use mspec as a benchmark to see if my tests make sense. Your tests read just fine with mspec, and that gives me some semi-automated warm fuzzies that I'm testing the correct things.

For that matter, you've done a better job with multiple asserts. I hate seeing tests that look like:

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

Because when that fails, the error message "expected True, got False" is completely worthless. Multiple asserts, and using the unit-testing framework as much as possible, will help you a great deal.

I agree with all the other comments that it is fine to do so, if you are logically testing one thing.

There is however a difference between have many assertions in a single unit test than having a separate unit test for each property. I call it 'Blocking Assertions' (Probably a better name out there). If you have many assertions in one test then you are only going to know about a failure in the first property that failed the assertion. If you have say 10 properties and 5 of them returned incorrect results then you will have to go through fixing the first one, re-run the test and notice another one failed, then fix that etc.

Depending on how you look at it this could be quite frustrating. On the flip side having 5 simple unit tests failing suddenly could also be off putting, but it might give you a clearer picture as to what have caused those to fail all at once and possibly direct you more quickly to a known fix (perhaps).

I would say if you need to test multiple properties keep the number down (possibly under 5) to avoid the blocking assertion issue getting out of control. If there are a ton of properties to test then perhaps it is a sign that your model is representing too much or perhaps you can look at grouping properties into multiple tests.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM