單元測試-xUnit總結

xUnit總結

什麼是xUnit

xUnit.net是針對.NET Framework的免費,開源,以社區爲中心的單元測試工具。git

自動化測試的優勢

  • 能夠頻繁的進行測試
  • 能夠在任什麼時候間進行測試,也能夠按計劃定時進行,例如:能夠在半夜進行自動化測試
  • 比人工測試速度快
  • 能夠更快速地發現錯誤
  • 基本上是很是可靠的
  • 測試代碼與生產代碼緊密結合
  • 使得開發團隊更具備幸福感

自動化測試的分類

單元測試能夠測試某個類或方法,具備較高的深度,對應用的功能覆蓋面很小。
集成測試有更好的廣度,能夠測試web資源,數據庫資源等。
皮下測試在web中針對controller下的節點測試。
UI測試是對應用的界面功能測試。
實際上經常使用的是單元測試和集成測試。web

是測試行爲仍是測試私有方法

通常是針對類的Public方法進行測試,也就是對行爲進行測試,若是是私有方法須要改變修飾符才能測試正則表達式

xUnit.Net特色:

  • 支持多平臺/運行時
  • 並行測試
  • 數據驅動測試
  • 可擴展

xUnit支持的平臺:

.Net Framework
.Net Core
.Net Standard
UWP
Xamarin數據庫

官網:
https://xunit.netc#

測試工具:

VS自帶的測試瀏覽器(右鍵測試或者ctrl+r,t)
resharper,
cmd命令行(.net cli): 
    dotnet test
        dotnet test --help

簡單的例子

  1. 在VS中建立一個解決方案,再建立一個.net core類庫:Demo,添加一個Calculator類:
namespace Demo
{
    public class Calculator
    {
        public int Add(int x,int y)
        {
            return x + y;
        }
    }
}
  1. 在同一解決方案,建立一個xUnit測試項目:DemoTest,針對項目測試,通常是項目名+Test命名測試項目。建立一個類:CalculatorTests:
public class CalculatorTests
{
    [Fact]
    public void ShouldAddEquals5() //注意命名規範
    {
        //Arrange
        var sut = new Calculator(); //sut-system under test,通用命名
        //Act
        var result = sut.Add(3, 2);
        //Assert
        Assert.Equal(5, result);

    }
}
  1. 運行測試(任意一種方法):
    1. 經過vs自帶的測試資源管理器,找到測試項目,選擇運行;

    1. 經過在ShouldAddEquals5方法上,右鍵選擇運行測試或者快捷鍵(ctrl+r,t)
    1. 經過cmd,在測試項目目錄運行dotnet test

    1. resharper(沒有安裝,太耗費內存)

測試的三個階段:AAA

Arrange: 在這裏作一些先決的設定。例如建立對象實例,數據,輸入等。
Act: 在這裏執行生產代碼並返回結果。例如調用方法或者設置屬性。
Assert:在這裏檢查結果,會產生測試經過或者失敗兩種結果。瀏覽器


Assert

Assert基於代碼的返回值、對象的最終狀態、事件是否發生等狀況來評估測試的結果
Assert的結果多是Pass或者Fail
若是全部的asserts都經過了,那麼整個測試就經過了。
若是任何assert 失敗了,那麼結果就失敗了。bash

一個test裏應該有多少個assertside

  1. 一種簡易的作法是,每一個test方法裏面只有一個assert.
  2. 而還有一種建議就是,每一個test裏面能夠有多個asserts,只要這些asserts都是針對同一個行爲。
    xUnit提供瞭如下類型的Assert:

Assert方法應用

演示示例:
先建一個.net core類庫項目,再創建一個xunit測試項目(參考最後綜合示例)函數

Assert.True,Assert.False

[Fact]
[Trait("Category","New")]
public void BeNewWhenCreated()
{
    _output.WriteLine("第一個測試");
    // Arrange
    var patient = new Patient();
    // Act
    var result = patient.IsNew;
    // Assert
    Assert.True(result);
}

字符串結果測試:Assert.Equal

[Fact]
public void HaveCorrectFullName()
{
    //var patient = new Patient();
    _patient.FirstName = "Nick";
    _patient.LastName = "Carter";
    var fullName = _patient.FullName;
    Assert.Equal("Nick Carter", fullName); //相等
    Assert.StartsWith("Nick", fullName);//以開頭
    Assert.EndsWith("Carter", fullName);//以結尾
    Assert.Contains("Carter", fullName);//包含
    Assert.Contains("Car", fullName);
    Assert.NotEqual("CAR", fullName);//不相等
    Assert.Matches(@"^[A-Z][a-z]*\s[A-Z][a-z]*", fullName);//正則表達式
}

數字結果測試

[Fact]
[Trait("Category", "New")]
public void HaveDefaultBloodSugarWhenCreated()
{
    var p = new Patient();
    var bloodSugar = p.BloodSugar;
    Assert.Equal(4.9f, bloodSugar,5); //判斷是否相等
    Assert.InRange(bloodSugar, 3.9, 6.1);//判斷是否在某一範圍內
}

判斷null,not null

[Fact]
public void HaveNoNameWhenCreated()
{
    var p = new Patient();
    Assert.Null(p.FirstName);
    Assert.NotNull(_patient);
}

集合測試

[Fact]
public void HaveHadAColdBefore()
{
    //Arrange
    var _patient = new Patient();
    
    //Act
    var diseases = new List<string>
    {
        "感冒",
        "發燒",
        "水痘",
        "腹瀉"
    };
    _patient.History.Add("發燒");
    _patient.History.Add("感冒");
    _patient.History.Add("水痘");
    _patient.History.Add("腹瀉");
    
    //Assert
    //判斷集合是否含有或者不含有某個元素
    Assert.Contains("感冒",_patient.History);
    Assert.DoesNotContain("心臟病", _patient.History);

    //判斷p.History至少有一個元素,該元素以水開頭
    Assert.Contains(_patient.History, x => x.StartsWith("水"));
    //判斷集合的長度
    Assert.All(_patient.History, x => Assert.True(x.Length >= 2));

    //判斷集合是否相等,這裏測試經過,說明是比較集合元素的值,而不是比較引用
    Assert.Equal(diseases, _patient.History);

}

測試對象

/// <summary>
/// 測試Object
/// </summary>
[Fact]
public void BeAPerson()
{
    var p = new Patient();
    var p2 = new Patient();
    Assert.IsNotType<Person>(p); //測試對象是否相等,注意這裏爲false
    Assert.IsType<Patient>(p);

    Assert.IsAssignableFrom<Person>(p);//判斷對象是否繼承自Person,true

    //判斷是否爲同一個實例
    Assert.NotSame(p, p2);
    //Assert.Same(p, p2);

}

判斷是否發生異常

/// <summary>
/// 判斷是否發生異常
/// </summary>
[Fact]
public void ThrowException() //注意不能使用ctrl+R,T快捷鍵,由於會中斷測試,拋出異常
{
    var p = new Patient();
    //判斷是否返回指定類型的異常
    var ex = Assert.Throws<InvalidOperationException>(()=> { p.NotAllowed(); });
    //判斷異常信息是否相等
    Assert.Equal("not able to create", ex.Message);
}

判斷是否觸發事件

/// <summary>
/// 判斷是否觸發事件
/// </summary>
[Fact]
public void RaizeSleepEvent()
{
    var p = new Patient();
    Assert.Raises<EventArgs>(
        handler=>p.PatientSlept+=handler,
        handler=>p.PatientSlept -= handler,
        () => p.Sleep());
}

判斷屬性改變是否觸發事件

/// <summary>
/// 測試屬性改變事件是否觸發
/// </summary>
[Fact]
public void RaisePropertyChangedEvent()
{
    var p = new Patient();
    Assert.PropertyChanged(p, nameof(p.HeartBeatRate),
                           () => p.IncreaseHeartBeatRate());
}

分組、忽略、log、共享上下文

測試分組

使用trait特性,對測試進行分組:[Trait("Name","Value")] 能夠做用於方法級和Class級別
相同的分組使用相同的特性。工具

[Fact]
[Trait("Category","New")]
public void BeNewWhenCreated()
{
    _output.WriteLine("第一個測試");
    // Arrange
    //var patient = new Patient();
    // Act
    var result = _patient.IsNew;
    // Assert
    Assert.True(result);
    //Assert.False(result);
}

測試分組搜索: 能夠在測試資源管理器中按分組排列、搜索、運行測試

在dotnet cli中分組測試:

dotnew test --filter 「Category=New」 //運行單個分類測試
dotnew test --filter 「Category=New|Category=Add」//運行多個分類測試
dotnet test --filter Category --logger:trx //輸出測試日誌

忽略測試

使用特性:[Fact(Skip="不跑這個測試")],能夠忽略測試,忽略測試圖標爲黃色警告

自定義測試輸出內容

使用ITestOutputHelper能夠自定義在測試時的輸出內容
dotnet test --filter Category --logger:trx會輸出測試日誌trx結尾的文件

public class PatientShould:IClassFixture<LongTimeFixture>,IDisposable
{
    private readonly ITestOutputHelper _output;
    private readonly Patient _patient;
    private readonly LongTimeTask _task;
    public PatientShould(ITestOutputHelper output,LongTimeFixture fixture)
    {
        this._output = output;
        _patient = new Patient();
        //_task = new LongTimeTask();
        _task = fixture.Task;
    }

    [Fact]
    [Trait("Category","New")]
    public void BeNewWhenCreated()
    {
        _output.WriteLine("第一個測試");
        // Arrange
        //var patient = new Patient();
        // Act
        var result = _patient.IsNew;
        // Assert
        Assert.True(result);
        //Assert.False(result);
    }
}

減小重複代碼

  1. 減小new對象,能夠在構造函數中new,在方法中使用。
  2. 測試類實現IDispose接口,測試完釋放資源,注意每一個測試結束後都會調用Dispose方法。

共享上下文

同一個測試類

在執行一個方法時,須要很長事件,而在構造函數中new時,每一個測試跑的時候都會new對象或者執行方法,這是致使測試很慢。解決方法:

  1. 建立一個類:
using Demo2;
using System;

namespace Demo2Test
{
    public class LongTimeFixture : IDisposable
    {
        public LongTimeTask Task { get; }
        public LongTimeFixture()
        {

        }
        public void Dispose()
        {
        }
    }
}
  1. 測試類實現IClassFixture<LongTimeFixture>接口,並在構造函數中獲取方法
public class PatientShould:IClassFixture<LongTimeFixture>,IDisposable
{
    private readonly ITestOutputHelper _output;
    private readonly Patient _patient;
    private readonly LongTimeTask _task;
    public PatientShould(ITestOutputHelper output,LongTimeFixture fixture)
    {
        this._output = output;
        _patient = new Patient();
        //_task = new LongTimeTask();
        _task = fixture.Task;//獲取方法
    }
}

不一樣的測試類

1.在上一個的繼承上,先創建一個TaskCollection類,實現ICollectionFixture<LongTimeFixture>接口,注意不能有反作用,不然會影響結果

using Xunit;

namespace Demo2Test
{
    [CollectionDefinition("Lone Time Task Collection")]
    public class TaskCollection:ICollectionFixture<LongTimeFixture>
    {
    }
}
  1. 使用,加上[Collection("Lone Time Task Collection")]
[Collection("Lone Time Task Collection")]
public class PatientShould:IClassFixture<LongTimeFixture>,IDisposable
{
    private readonly ITestOutputHelper _output;
    private readonly Patient _patient;
    private readonly LongTimeTask _task;
    public PatientShould(ITestOutputHelper output,LongTimeFixture fixture)
    {
        this._output = output;
        _patient = new Patient();
        //_task = new LongTimeTask();
        _task = fixture.Task;//獲取方法
    }
}

數據共享

1. 使用[Theory],能夠寫有構造參數的測試方法,使用InlineData傳遞數據

[Theory]
[InlineData(1,2,3)]
[InlineData(2,2,4)]
[InlineData(3,3,6)]
public void ShouldAddEquals(int operand1,int operand2,int expected)
{
    //Arrange
    var sut = new Calculator(); //sut-system under test
    //Act
    var result = sut.Add(operand1, operand2);
    //Assert
    Assert.Equal(expected, result);
}

2. 使用[MemberData]特性,能夠在多個測試中使用

  1. 先添加CalculatorTestData類:
using System.Collections.Generic;

namespace DemoTest
{
    public  class CalculatorTestData
    {
        private static readonly List<object[]> Data = new List<object[]>
        {
            new object[]{ 1,2,3},
            new object[]{ 1,3,4},
            new object[]{ 2,4,6},
            new object[]{ 0,1,1},
        };

        public static IEnumerable<object[]> TestData => Data;
    }
}
  1. 使用MemberData
/// <summary>
/// 數據共享-MemberData
/// </summary>
/// <param name="operand1"></param>
/// <param name="operand2"></param>
/// <param name="expected"></param>
[Theory]
[MemberData(nameof(CalculatorTestData.TestData),MemberType =typeof(CalculatorTestData))]
public void ShouldAddEquals2(int operand1, int operand2, int expected)
{
    //Arrange
    var sut = new Calculator(); //sut-system under test
    //Act
    var result = sut.Add(operand1, operand2);
    //Assert
    Assert.Equal(expected, result);
}

3. 使用外部數據

  1. 先建立一個類,準備數據,這裏是讀取的csv文件的數據
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace DemoTest.Data
{
    /// <summary>
    /// 讀取文件並返回數據集合
    /// </summary>
    public class CalculatorCsvData
    {
        public static IEnumerable<object[]> TestData
        {
            get
            {
                //把csv文件中的數據讀出來,轉換
                string[] csvLines = File.ReadAllLines("Data\\TestData.csv");
                var testCases = new List<object[]>();
                foreach (var csvLine in csvLines)
                {
                    IEnumerable<int> values = csvLine.Trim().Split(',').Select(int.Parse);
                    object[] testCase = values.Cast<object>().ToArray();
                    testCases.Add(testCase);
                }
                return testCases;
            }
        }
    }
}
  1. csv數據
1,2,3
1,3,4
2,4,6
0,1,1
  1. 使用
/// <summary>
/// 數據共享-MemberData-外部數據
/// </summary>
/// <param name="operand1"></param>
/// <param name="operand2"></param>
/// <param name="expected"></param>
[Theory]
[MemberData(nameof(CalculatorCsvData.TestData), MemberType = typeof(CalculatorCsvData))]
public void ShouldAddEquals3(int operand1, int operand2, int expected)
{
    //Arrange
    var sut = new Calculator(); //sut-system under test
    //Act
    var result = sut.Add(operand1, operand2);
    //Assert
    Assert.Equal(expected, result);
}

4. 使用自定義特性,繼承自DataAttribute

  1. 自定義特性
using System.Collections.Generic;
using System.Reflection;
using Xunit.Sdk;

namespace DemoTest.Data
{
    public class CalculatorDataAttribute : DataAttribute
    {
        public override IEnumerable<object[]> GetData(MethodInfo testMethod)
        {
            yield return new object[] { 0, 100, 100 };
            yield return new object[] { 1, 99, 100 };
            yield return new object[] { 2, 98, 100 };
            yield return new object[] { 3, 97, 100 };
        }
    }
}
  1. 使用
/// <summary>
/// 數據共享-自定義特性繼承自DataAttribute
/// </summary>
/// <param name="operand1"></param>
/// <param name="operand2"></param>
/// <param name="expected"></param>
[Theory]
[CalculatorDataAttribute]
public void ShouldAddEquals4(int operand1, int operand2, int expected)
{
    //Arrange
    var sut = new Calculator(); //sut-system under test
    //Act
    var result = sut.Add(operand1, operand2);
    //Assert
    Assert.Equal(expected, result);
}

源碼:https://gitee.com/Alexander360/LearnXUnit

相關文章
相關標籤/搜索