Search code examples
tddnunitbddmspecxunit

Can these row test style unit tests be improved to follow good TDD design practices?


Can the following unittest be improved, to follow good TDD design practises (naming, using rowtests, designing the classes) in any of the .NET TDD/BDD frameworks?

Also, is there a better way in any of the frameworks to have rowtests where I can have a individual expectation for each row, just like I do it in this (NUnit) example?

The system under test here is the Constraint class that can have multiple ranges of valid integers. The test test the NarrowDown method that can make the valid ranges smaller based on another constraint.

[TestFixture]
internal class ConstraintTests
{
    [Test]
    public void NarrowDown_Works()
    {
        RowTest_NarrowDown(
            new Range[] { new Range(0, 10), new Range(20, 30), new Range(40, 50) },
            new Range[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) },
            new Range[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) });

        RowTest_NarrowDown(
            new Range[] { new Range(0, 10), new Range(20, 30), new Range(40, 50), new Range(60, 70) },
            new Range[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) },
            new Range[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) });

        RowTest_NarrowDown(
            new Range[] { new Range(0, 10), new Range(20, 30), new Range(40, 50) },
            new Range[] { new Range(1, 9), new Range(21, 29), new Range(41, 49), new Range(60, 70) });
    }

    private static void RowTest_NarrowDown(IEnumerable<Range> sut, IEnumerable<Range> context)
    {
        Constraint constraint = new Constraint(sut);
        Constraint result = constraint.NarrowDown(new Constraint(context));
        Assert.That(result, Is.Null);
    }

    private static void RowTest_NarrowDown(IEnumerable<Range> sut, IEnumerable<Range> context, IEnumerable<Range> expected)
    {
        Constraint constraint = new Constraint(sut);
        Constraint result = constraint.NarrowDown(new Constraint(context));
        Assert.That(result, Is.Not.Null);
        Assert.That(result.Bounds, Is.EquivalentTo(expected));
    }
}

Solution

  • You should use a data-driven approach with data factories (in NUnit-speak, they're called test case sources). This makes your tests a lot easier to read, understand, modify and maintain (or, more generally, a lot cleaner):

    [TestFixture]
    internal class ConstraintTests
    {
        static object[] TwoRanges = 
        {
            new object[]
                {
                    new[] { new Range(0, 10), new Range(20, 30), new Range(40, 50) },
                    new[] { new Range(1, 9), new Range(21, 29), new Range(41, 49), new Range(60, 70) }
                }
        };
    
        static object[] ThreeRanges = 
        {
            new object[]
                {
                    new[] { new Range(0, 10), new Range(20, 30), new Range(40, 50) },
                    new[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) },
                    new[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) }
                },
            new object[]
                {
                    new[] { new Range(0, 10), new Range(20, 30), new Range(40, 50), new Range(60, 70) },
                    new[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) },
                    new[] { new Range(1, 9), new Range(21, 29), new Range(41, 49) }
                }
        };
    
        [Test, TestCaseSource("TwoRanges")]
        public void NarrowDown_WhenCalledWithTwoRanges_GivesTheExpectedResult(IEnumerable<Range> sut, IEnumerable<Range> context)
        {
            Constraint constraint = new Constraint(sut);
            Constraint result = constraint.NarrowDown(new Constraint(context));
            Assert.That(result, Is.Null);
        }
    
        [Test, TestCaseSource("ThreeRanges")]
        public void NarrowDown_WhenCalledWithThreeRanges_GivesTheExpectedResult(IEnumerable<Range> sut, IEnumerable<Range> context, IEnumerable<Range> expected)
        {
            Constraint constraint = new Constraint(sut);
            Constraint result = constraint.NarrowDown(new Constraint(context));
            Assert.That(result, Is.Not.Null);
            Assert.That(result.Bounds, Is.EquivalentTo(expected));
        }
    }
    

    See how much simpler your test methods have become now? Also, this will make each set of data from the originating test case source run in a separate test, so the whole thing won't fail only because one set of data causes a failure. Remember: A test should assert only one thing.

    HTH!