[C#] 初探 XUnits

Unit Testing with xUnit

單元測試以前都沒寫過,上次去外面上課,稍微接觸一下。由於最近在網路上買了一本 .Net Core 來看,感覺 Unit Test 是一個好東西。 所以,我得做一點筆記,紀錄一下,不然哪天又忘了。

Unit Test

在軟體工程中,有一個 SRP Pattern ,大意是盡量讓一個類別維持單一原則,不要做太多事。這時候,我們就可以透過單元測試,來個別驗證這些類別與功能。 單元測試可以提高軟體的可靠度,減少將來軟體釋出時,帶來的一些臭蟲。

Business day Calculator Example

書上給了一些範例,就套用這範例,紀錄下去吧!

透過 VS 建立一個空方案(BusinessDays),接著新增一個 Lib 專案(BizDayCal),在新增一個 xUnit 專案(BizDayCalTests)。

BizDayCal

namespace BizDayCal 
{
    using System;
    using System.Collections.Generic;

    public class Calculator {
        private List<IRule> rules = new List<IRule>();

        public void AddRule(IRule rule) 
        {
            this.rules.Add(rule);
        }

        public bool IsBusinessDay(DateTime date)
        {
            foreach (var rule in rules)
                if (!rule.CheckDate(date))
                    return false;
            return true;
        }
    }
}

namespace BizDayCal
{
    using System;

    public Interface IRule
    {
        bool CheckDate(DateTime date);
    }
}

namespace BizDayCal
{
    using System;

    public class WeekendRule : IRule
    {
        public bool CheckDate(DateTime date)
        {
            return date.DayofWeek != DayofWeek.Sunday && date.DayofWeek != DayofWeek.Saturday;
        }
    }
}

BizDayCalTests

建立 BizDayCalTests 專案時,記得將 BizDayCal 專案加入參考。 使用一般的方法來驗證方法。/Fact, Assert/

namespace BizDayCalTests
{
    using System;
    using System.Collections.Generic;
    using BizDayCal;
    using xUnit;

    public class WeekendRuleTest
    {
        [Fact]
        public void TestCheckDay()
        {
            var rule = new WeekendRule();
            Assert.True(rule.CheckDate(new DateTime(2017, 12, 31));
            // or 
            Assert.False(rule.CheckDate(new DateTime(2018, 01, 01));
        }
    }
}

使用 Theory

如果我們依據一般的方法,相同的邏輯,很同類似的測試範例,可能得寫一堆測試的範例,這時候,可以使用 Theory 屬性來做測試。 Note : 使用 Theory 只能斷定測試的結果是否為真。

namespace BizDayCalTests
{
    using System;
    using System.Collections.Generic;
    using BizDayCal;
    using xUnit;

    public class WeekendRuleTest
    {
        [Theory]
        [InlineData("2018-01-03")]
        [InlineData("2018-01-04")]
        public void IsBusinessDay(string date)
        {
            var rule = new WeekendRule();
            Assert.True(rule.CheckDate(DateTime.Parse(date));
        }

        [Theory]
        [InlineData("2017-12-31")]
        public void IsNotBusinessDay(string date)
        {
            var rule = new WeekendRule();
            Assert.False(rule.CheckDate(DateTime.Parse(date));
        }
    }
}

上面的範例可以看到,使用了一個 InlineData 的屬性,屬性的內容將會當作測試的樣本傳入該測試方法做測試。 另外,也可以將測試的預期結果,放在 InlineData 裡面。

namespace BizDayCalTests
{
    using System;
    using System.Collections.Generic;
    using BizDayCal;
    using xUnit;

    public class WeekendRuleTest
    {
        [Theory]
        [InlineData(true, "2018-01-03")]
        [InlineData(true, "2018-01-04")]
        [InlineData(false, "2017-12-31")]
        public void IsBusinessDay(bool expected, string date)
        {
            var rule = new WeekendRule();
            Assert.Equal(expected, rule.CheckDate(DateTime.Parse(date)));
        }
    }
}

Theories with MemberData

依據上面的範例,可能需要定義很多個屬性來作為測試的範例。如果不想定義太多個測試屬性的話,可以考慮使用 MemberData。 MemberData 只能被運用在靜態屬性上。

namespace BizDayCalTests
{
    using System;
    using System.Collections.Generic;
    using BizDayCal;
    using xUnit;

    public class WeekendRuleTest
    {
        public static IEnumerable<object[]> Days {
            get {
                yield return new object[] {true, new DateTime(2018, 1, 3)};
                yield return new object[] {true, new DateTime(2018, 1, 4)};
                yield return new object[] {true, new DateTime(2017, 12, 31)};
            }
        }

        [Theory]
        [MemberData(nameof(Days))]
        public void TestCheckDate(bool expected, string date)
        {
            var rule = new WeekendRule();
            Assert.Equal(expected, rule.CheckDate(date));
        }
    }
}

在測試條件中共用相同的測試集內容

使用建構子

找了 2018 台灣的放假日期,來當測試範例。

namespace BizDayCal
{
    public class HolidayRule : IRule
    {
        public static readonly int[,] TW2018Holidays = {
            { 1, 1},
            { 2, 15}, { 2, 16}, { 2, 17}, { 2, 18}, { 2, 19},{ 2, 20},
            { 4, 4}, { 4, 5}, { 4, 6}, { 4, 7}, { 4, 8},
            { 6, 16}, { 6, 17}, { 6, 18},
            { 9, 22}, { 9, 23}, { 9, 24},
            { 12, 29}, { 12, 30}, { 12, 30}
        };

        public void CheckDate(DateTime date)
        {
            for (int day = 0 ; day <= TW2018Holidays.GetUpperBound(0); day++) {
                if (date.Month == TW2018Holidays[day, 0] && date.Day == TW2018Holidays[day, 1])
                    return false;
            }

            return true;
        }
    }
}

namespace BizDayCalTests
{
    public class TW2018HolidaysTest
    {
        public static IEnumerable<object[]> Holidays {
            get {
                yield return new object[] { new DateTime(2018, 1, 1};
                yield return new object[] { new DateTime(2018, 2, 15};
                yield return new object[] { new DateTime(2018, 4, 4};
                yield return new object[] { new DateTime(2018, 6, 16};
                yield return new object[] { new DateTime(2018, 9, 22};
                yield return new object[] { new DateTime(2018, 12, 29};
            }
        }

        private Calculator calculator;

        public TW2018HolidaysTest()
        {
            calculator = new calculator();
            calculator.AddRule(new HolidayRule());
        }

        [Theory]
        [MemberData(nameof(Holidays))]
        public void TestHolidays(DateTime date)
        {
            Assert.False(calculator.IsBusinessDay(date));
        }

        [Theory]
        [InlineData("2018-01-03")]
        [InlineData("2018-01-04")]
        public void TestNotHolidays(string date)
        {
            Assert.True(calculator.IsBusinessDay(new DateTime.Parse(date)));
        }
    }
}

Class Fixtures

有一些測試範例,如果只想執行一次或是重複使用測試集,xUnit 提供一個介面 IClassFixture。

namespace BizDayCalTests
{
    public class TWRegionFixture
    {
        public Calculator Calc {get; private set;}

        public TWRegionFixture()
        {
            Calc = new Calculator();
            Calc.AddRule(new WeekendRule());
            Calc.AddRule(new HolidayRule());
        }
    }
}

namespace BizDayCalTests
{
    using System;
    using BizDayCal;
    using Xunit;

    public class TWRegionFixtureTest : IClassFixture<TWRegionFixture>
    {
        private TWRegionFixture _fixture;

        public TWRegionFixtureTest(TWRegionFixture fixture)
        {
            this._fixture = fixture;
        }

        [Theory]
        [InlineData("2018-01-01")]
        [InlineData("2018-12-25")]
        public void TestHolidays(string date)
        {
            Assert.False(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }

        [Theory]
        [InlineData("2018-01-03")]
        [InlineData("2018-01-04")]
        public void TestNotHolidays(string date)
        {
            Assert.True(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }
    }
}

使用 IClassFixture 可以發現到,建構子只會被建立一次而已。

Using Collections

如果測試的資料集要與其他測試範例共用時,可以考慮使用 Collections 的方式。

namespace BizDayCalTests
{
    public class TWRegionFixture
    {
        public Calculator Calc {get; private set;}

        public TWRegionFixture()
        {
            Calc = new Calculator();
            Calc.AddRule(new WeekendRule());
            Calc.AddRule(new HolidayRule());
        }
    }

    [CollectionDefinition("TW Region Collection")]
    public class TWRegionCollection : ICollectionFixture<TWRegionFixture>
    {}
}

namespace BizDayCalTests
{
    using System;
    using BizDayCal;
    using Xunit;

    [Collection("TW Region Collection")]
    public class TWRegionTest
    {
        private TWRegionFixture _fixture;

        public TWRegionTest(TWRegionFixture fixture)
        {
            this._fixture = fixture;
        }

        [Theory]
        [InlineData("2018-01-01")]
        [InlineData("2018-12-25")]
        public void TestHolidays(string date)
        {
            Assert.False(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }

        [Theory]
        [InlineData("2018-01-03")]
        [InlineData("2018-01-04")]
        public void TestNotHolidays(string date)
        {
            Assert.True(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }
    }
}

Collection 與 CollectionDefinition 的內容要一致。

取得測試的輸出結果

想要清楚的看見測試的結果,可以透過加入 Xunit.Abstractions 並且注入 ITestOutputHelper。

namespace BizDayCalTests
{
    using System;
    using BizDayCal;
    using Xunit;
    using Xunit.Abstractions;   

    [Collection("TW Region Collection")]
    public class TWRegionTest
    {
        private readonly TWRegionFixture _fixture;
        private readonly ITestOutputHelper _output;

        public TWRegionTest(TWRegionFixture fixture, ITestOutputHelper output)
        {
            this._fixture = fixture;
            this._output = output;
        }

        [Theory]
        [InlineData("2018-01-01")]
        [InlineData("2018-12-25")]
        public void TestHolidays(string date)
        {
            _output.WriteLine($@"TestHolidays(""{date}"")");
            Assert.False(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }

        [Theory]
        [InlineData("2018-01-03")]
        [InlineData("2018-01-04")]
        public void TestNotHolidays(string date)
        {
            _output.WriteLine($@"TestNotHolidays(""{date}"")");
            Assert.True(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }
    }
}

Traits

Traits 允許配置大量的屬性來做測試。Traits 是成對的 name-value。

        [Theory]
        [InlineData("2018-01-01")]
        [InlineData("2018-12-25")]
        [Traits("Holiday", "true")]
        public void TestHolidays(string date)
        {
            Assert.False(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }

        [Theory]
        [InlineData("2018-01-03")]
        [InlineData("2018-01-04")]
        [Traits("Holiday", "false")]
        public void TestNotHolidays(string date)
        {
            Assert.True(this._fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
        }

留言

這個網誌中的熱門文章

[Tools] GCOV & LCOV 初探

Quilt Patch 管理操作方法

[C#]C# Coding 規則