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

Leave a Reply

avatar
  Subscribe  
Notify of