Skip to main content

10分鐘學會XUnit

又到了歡樂的十分鐘系列(上次出十分鐘系列是多久以前了啊....)

XUnit是一套非常流行的測試框架,很多人常用的是NUnit,但最近在研究整合測試時發現MVC的整合測試幾乎都是用XUnit在寫,所以就生出了這篇文。

基本上,我認為XUnit比NUnit好上手,如果你熟悉相依注入的話,那麼更會覺得XUnit設計的觀念非常直覺。

基本語法

在方法身上掛上[Fact]就變成測試test case了

namespace XUnitTestProject1
{
public class StackTests
{
private readonly Stack<int> _Stack;
public StackTests()
{
_Stack = new Stack<int>();
}
[Fact]
public void stack_should_be_empty()
{
Assert.Empty( _Stack);
}
[Fact]
public void stack_count_should_be_2_after_pushing_two_items()
{
_Stack.Push(42);
_Stack.Push(17);
Assert.Equal(2, _Stack.Count);
}
}
}

實際執行的時候,會產生兩個StackTests物件,分別執行這兩個test case,這樣才不會打架。所以請不要使用static,不然測試之間就不獨立了。

巢狀測試

public class OutsideTests
{
public class InnerTests
{
[Fact]
public void inner_test_case_1()
{
Assert.True(true);
}
}
[Fact]
public void outer_test_case_1()
{
Assert.True(true);
}
}

巢狀測試也可以,顯示時會自動攤平

輸出資訊

在XUnit 2.0之後,因為預設會開啟平行跑測試,Console.WriteLine是沒有用的,必須要在測試類的constructor注入ITestOutputHelper,才能夠輸出自訂內容。

public class Printer
{
private readonly ITestOutputHelper _output;
public Printer(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void output()
{
Console.WriteLine("This is not work"); // this not work
_output.WriteLine("Hello XUnit"); // this works
}
}

在多個測試間共享一個實體

有時測試會相依於一個建立時會很消耗資源的物件,例如資料庫連線。如果能在多個測試間使用同一個實體,就可以加快測試的速度。

下述範例中,我們建立一個ListFixture扮演耗資源物件,並撰寫兩個TestFixture,MyTestFixture1與MyTestFixture2,其中MyTestFixture1的所有test case會共用同一份ListFixture instance,而MyTestFixture2自己的測試也會有屬於自己的instace。

public class ListFixture
{
public readonly List<int> _Numbers;
public ListFixture()
{
_Numbers = new List<int>();
}
public void AddNumber(int num)
{
_Numbers.Add(num);
}
}
// 只要實作IClassFixture介面,XUnit會替該類別的每個test case相依注入同一份物件。
public class MyTestFixture1 : IClassFixture<ListFixture>
{
private readonly ListFixture _fixture;
private readonly ITestOutputHelper _output;
public MyTestFixture1(ListFixture fixture, ITestOutputHelper output)
{
this._fixture = fixture;
_output = output;
}
// 為了執行test_case_1會產生一個新的MyTestFixture1 instance,然後會注入ListFixture,為了執行test_case_2也會產生一個新的MyTestFixture1 instance,但注入的是同一份ListFixture instance
[Fact]
public void test_case_1()
{
_fixture.AddNumber(100);
foreach (var number in _fixture._Numbers)
{
_output.WriteLine(number.ToString()); // 200, 100
}
}
[Fact]
public void test_case_2()
{
_fixture.AddNumber(200);
foreach (var number in _fixture._Numbers)
{
_output.WriteLine(number.ToString()); // 200
}
}
}
public class MyTestFixture2 : IClassFixture<ListFixture>
{
private readonly ListFixture _fixture;
private readonly ITestOutputHelper _output;
// 這裡注入一個新的fixture實體。
public MyTestFixture2(ListFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_output = output;
}
[Fact]
public void fixture_2_test_case()
{
_fixture.AddNumber(500);
foreach (var number in _fixture._Numbers)
{
_output.WriteLine(number.ToString()); // 500
}
}
}

釋放資源

如果想要在測試物件再也不會被使用時釋放掉資源,可以實作IDisposable介面。

public class ListFixture: IDisposable
{
public readonly List<int> _Numbers;
public ListFixture()
{
_Numbers = new List<int>();
}
public void AddNumber(int num)
{
_Numbers.Add(num);
}
public void Dispose()
{
// do something release resource
}
}

在多組測試間共用同一份相依物件

public class ListFixture : IDisposable
{
public readonly List<int> _Numbers;
public ListFixture()
{
_Numbers = new List<int>();
}
public void Dispose()
{
// do something release resource
}
public void AddNumber(int num)
{
_Numbers.Add(num);
}
}
// put **CollectionDefinition** here
[CollectionDefinition("Awesome Collection")]
public class AweSome: ICollectionFixture<ListFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces
}
// put **Collection** here
[Collection("Awesome Collection")]
public class MyTestFixture1 // no need implement any interface
{
private readonly ListFixture _fixture;
private readonly ITestOutputHelper _output;
public MyTestFixture1(ListFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_output = output;
}
[Fact]
public void test_case_1()
{
_fixture.AddNumber(100);
foreach (var number in _fixture._Numbers)
_output.WriteLine(number.ToString()); // 200 100
}
[Fact]
public void test_case_2()
{
_fixture.AddNumber(200);
foreach (var number in _fixture._Numbers)
_output.WriteLine(number.ToString()); // 200
}
}
[Collection("Awesome Collection")]
public class MyTestFixture2
{
private readonly ListFixture _fixture;
private readonly ITestOutputHelper _output;
public MyTestFixture2(ListFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_output = output;
}
[Fact]
public void fixture_2_test_case()
{
_fixture.AddNumber(500);
foreach (var number in _fixture._Numbers)
_output.WriteLine(number.ToString()); // 200 100 500
}
}

平行執行

XUnit的一大特色是平行執行,了解這個就等於掌握XUnit的核心。

  1. 同一個class的test case會一個一個執行,不同class的test case會平行執行
  2. 同一個Collection的test case會一個一個執行,不同Collection的test case會平行執行

當然,相同test collection內的執行順序是沒有保證的,請養成讓測試簡單獨立的好習慣。

會跑五秒

public class TestClass1
{
[Fact]
public void Test1()
{
Thread.Sleep(3000);
}
}
public class TestClass2
{
[Fact]
public void Test2()
{
Thread.Sleep(5000);
}
}

會跑八秒

[Collection("Our Test Collection #1")]
public class TestClass1
{
[Fact]
public void Test1()
{
Thread.Sleep(3000);
}
}
[Collection("Our Test Collection #1")]
public class TestClass2
{
[Fact]
public void Test2()
{
Thread.Sleep(5000);
}
}

Reference:

  1. XUnit Official document