又到了歡樂的十分鐘系列(上次出十分鐘系列是多久以前了啊....
XUnit是一套非常流行的測試框架,很多人常用的是NUnit,但最近在研究整合測試時發現MVC的整合測試幾乎都是用XUnit在寫,所以就生出了這篇文。
基本上,我認為XUnit比NUnit好上手,如果你熟悉相依注入的話,那麼更會覺得XUnit設計的觀念非常直覺。
基本語法
在方法身上掛上[Fact]就變成測試test case了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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,不然測試之間就不獨立了。
巢狀測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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,才能夠輸出自訂內容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
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介面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 } } |
在多組測試間共用同一份相依物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
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的核心。
- 同一個class的test case會一個一個執行,不同class的test case會平行執行
- 同一個Collection的test case會一個一個執行,不同Collection的test case會平行執行
當然,相同test collection內的執行順序是沒有保證的,請養成讓測試簡單獨立的好習慣。
會跑五秒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class TestClass1 { [Fact] public void Test1() { Thread.Sleep(3000); } } public class TestClass2 { [Fact] public void Test2() { Thread.Sleep(5000); } } |
會跑八秒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[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: