xUnit總結
什么是xUnit
xUnit.net是針對.NET Framework的免費,開源,以社區為中心的單元測試工具。
自動化測試的優點
- 可以頻繁的進行測試
- 可以在任何時間進行測試,也可以按計划定時進行,例如:可以在半夜進行自動化測試
- 比人工測試速度快
- 可以更快速地發現錯誤
- 基本上是非常可靠的
- 測試代碼與生產代碼緊密結合
- 使得開發團隊更具有幸福感
自動化測試的分類
單元測試可以測試某個類或方法,具有較高的深度,對應用的功能覆蓋面很小。
集成測試有更好的廣度,可以測試web資源,數據庫資源等。
皮下測試在web中針對controller下的節點測試。
UI測試是對應用的界面功能測試。
實際上常用的是單元測試和集成測試。
是測試行為還是測試私有方法
一般是針對類的Public方法進行測試,也就是對行為進行測試,如果是私有方法需要改變修飾符才能測試
xUnit.Net特點:
- 支持多平台/運行時
- 並行測試
- 數據驅動測試
- 可擴展
xUnit支持的平台:
.Net Framework
.Net Core
.Net Standard
UWP
Xamarin
測試工具:
VS自帶的測試瀏覽器(右鍵測試或者ctrl+r,t)
resharper,
cmd命令行(.net cli):
dotnet test
dotnet test --help
簡單的例子
- 在VS中創建一個解決方案,再創建一個.net core類庫:Demo,添加一個Calculator類:
namespace Demo
{
public class Calculator
{
public int Add(int x,int y)
{
return x + y;
}
}
}
- 在同一解決方案,創建一個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);
}
}
- 運行測試(任意一種方法):
-
- 通過vs自帶的測試資源管理器,找到測試項目,選擇運行;
-
- 通過在ShouldAddEquals5方法上,右鍵選擇運行測試或者快捷鍵(ctrl+r,t)
-
- 通過cmd,在測試項目目錄運行dotnet test
-
- resharper(沒有安裝,太耗費內存)
測試的三個階段:AAA
Arrange: 在這里做一些先決的設定。例如創建對象實例,數據,輸入等。
Act: 在這里執行生產代碼並返回結果。例如調用方法或者設置屬性。
Assert:在這里檢查結果,會產生測試通過或者失敗兩種結果。
Assert
Assert基於代碼的返回值、對象的最終狀態、事件是否發生等情況來評估測試的結果
Assert的結果可能是Pass或者Fail
如果所有的asserts都通過了,那么整個測試就通過了。
如果任何assert 失敗了,那么結果就失敗了。
一個test里應該有多少個asserts
- 一種簡易的做法是,每個test方法里面只有一個assert.
- 而還有一種建議就是,每個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);
}
}
減少重復代碼
- 減少new對象,可以在構造函數中new,在方法中使用。
- 測試類實現IDispose接口,測試完釋放資源,注意每個測試結束后都會調用Dispose方法。
共享上下文
同一個測試類
在執行一個方法時,需要很長事件,而在構造函數中new時,每個測試跑的時候都會new對象或者執行方法,這是導致測試很慢。解決方法:
- 創建一個類:
using Demo2;
using System;
namespace Demo2Test
{
public class LongTimeFixture : IDisposable
{
public LongTimeTask Task { get; }
public LongTimeFixture()
{
}
public void Dispose()
{
}
}
}
- 測試類實現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>
{
}
}
- 使用,加上[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]特性,可以在多個測試中使用
- 先添加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;
}
}
- 使用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. 使用外部數據
- 先創建一個類,准備數據,這里是讀取的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;
}
}
}
}
- csv數據
1,2,3
1,3,4
2,4,6
0,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
- 自定義特性
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 };
}
}
}
- 使用
/// <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);
}