[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)));
}
留言
張貼留言