第一部分: http://www.cnblogs.com/cgzl/p/8283610.html
Assert
Assert做什么?Assert基於代碼的返回值、對象的最終狀態、事件是否發生等情況來評估測試的結果。Assert的結果可能是Pass或者Fail。如果所有的asserts都pass了,那么整個測試就pass了;如果有任何assert fail了,那么測試就fail了。
xUnit提供了以下類型的Assert:
- boolean:True/False
- String:相等/不等,是否為空,以..開始/結束,是否包含子字符串,匹配正則表達式
- 數值型:相等/不等,是否在某個范圍內,浮點的精度
- Collection:內容是否相等,是否包含某個元素,是否包含滿足某種條件(predicate)的元素,是否所有的元素都滿足某個assert
- Raised events:Custom events,Framework events(例如:PropertyChanged)
- Object Type:是否是某種類型,是否某種類型或繼承與某種類型
一個test里應該有多少個asserts?
一種建議的做法是,每個test方法里面只有一個assert。
而還有一種建議就是,每個test里面可以有多個asserts,只要這些asserts都是針對同一個行為就行。
第一個Assert
目標類:
public class Patient
{
public Patient()
{
IsNew = true;
}
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
public int HeartBeatRate { get; set; }
public bool IsNew { get; set; }
public void IncreaseHeartBeatRate()
{
HeartBeatRate = CalculateHeartBeatRate() + 2;
}
private int CalculateHeartBeatRate()
{
var random = new Random();
return random.Next(1, 100);
}
}
測試類:
public class PatientShould
{
[Fact]
public void HaveHeartBeatWhenNew()
{
var patient = new Patient();
Assert.True(patient.IsNew);
}
}
運行測試:

結果符合預期,測試通過。
改為Assert.False()的話:

測試Fail。
String Assert
測試string是否相等:
[Fact]
public void CalculateFullName() { var p = new Patient { FirstName = "Nick", LastName = "Carter" }; Assert.Equal("Nick Carter", p.FullName); }
然后你需要Build一下,這樣VS Test Explorer才能發現新的test。
運行測試,結果Pass:

同樣改一下Patient類(別忘了Build一下),讓結果失敗:

從失敗信息可以看到期待值和實際值。
StartsWith, EndsWith
[Fact]
public void CalculateFullNameStartsWithFirstName() { var p = new Patient { FirstName = "Nick", LastName = "Carter" }; Assert.StartsWith("Nick", p.FullName); } [Fact] public void CalculateFullNameEndsWithFirstName() { var p = new Patient { FirstName = "Nick", LastName = "Carter" }; Assert.EndsWith("Carter", p.FullName);e); }
Build,然后Run Test,結果Pass:

忽略大小寫 ignoreCase:
string默認的Assert是區分大小寫的,這樣就會失敗:

可以為這些方法添加一個參數ignoreCase設置為true,就會忽略大小寫:

包含子字符串 Contains
[Fact]
public void CalculateFullNameSubstring() { var p = new Patient { FirstName = "Nick", LastName = "Carter" }; Assert.Contains("ck Ca", p.FullName); }
Build,測試結果Pass。
正則表達式,Matches
測試一下First name和Last name的首字母是不是大寫的:
[Fact]
public void CalculcateFullNameWithTitleCase() { var p = new Patient { FirstName = "Nick", LastName = "Carter" }; Assert.Matches("[A-Z]{1}{a-z}+ [A-Z]{1}[a-z]+", p.FullName); }
Build,測試通過。
數值 Assert
首先為Patient類添加一個property: BloodSugar。
public class Patient
{
public Patient() { IsNew = true; _bloodSugar = 5.0f; } private float _bloodSugar; public float BloodSugar { get { return _bloodSugar; } set { _bloodSugar = value; } } ...
Equal:
[Fact]
public void BloodSugarStartWithDefaultValue() { var p = new Patient(); Assert.Equal(5.0, p.BloodSugar); }
Build,測試通過。
范圍, InRange:
首先為Patient類添加一個方法,病人吃飯之后血糖升高:
public void HaveDinner()
{
var random = new Random(); _bloodSugar += (float)random.Next(1, 1000) / 100; // 應該是1000 }
添加test:
[Fact]
public void BloodSugarIncreaseAfterDinner() { var p = new Patient(); p.HaveDinner(); // Assert.InRange<float>(p.BloodSugar, 5, 6); Assert.InRange(p.BloodSugar, 5, 6); }
Build,Run Test,結果Fail:

可以看到期待的Range和實際的值,這樣很好。如果你使用Assert.True(xx >= 5 && xx <= 6)的話,錯誤信息只能顯示True或者False。
因為HaveDinner方法里,表達式的分母應該是1000,修改后,Build,Run,測試Pass。
浮點型數值的Assert
在被測項目添加這兩個類:
namespace Hospital { public abstract class Worker { public string Name { get; set; } public abstract double TotalReward { get; } public abstract double Hours { get; } public double Salary => TotalReward / Hours; } public class Plumber : Worker { public override double TotalReward => 200; public override double Hours => 3; } }
然后針對Plumber建立一個測試類 PlumberShould.cs, 並建立第一個test:
namespace Hospital.Tests { public class PlumberShould { [Fact] public void HaveCorrectSalary() { var plumber = new Plumber(); Assert.Equal(66.666, plumber.Salary); } } }
Build項目, 然后再Test Explorer里面選擇按Class分類顯示Tests:

Run Selected Test, 結果會失敗:

這是一個精度的問題.
在Assert.Equal方法, 可以添加一個precision參數, 設置精度為3:
[Fact] public void HaveCorrectSalary() { var plumber = new Plumber(); Assert.Equal(66.666, plumber.Salary, precision: 3); }
Build, Run Test:

因為有四舍五入的問題, 所以test仍然fail了.
所以還需要改一下:
[Fact] public void HaveCorrectSalary() { var plumber = new Plumber(); Assert.Equal(66.667, plumber.Salary, precision: 3); }
這次會pass的:

Assert Null值
[Fact] public void NotHaveNameByDefault() { var plumber = new Plumber(); Assert.Null(plumber.Name); } [Fact] public void HaveNameValue() { var plumber = new Plumber { Name = "Brian" }; Assert.NotNull(plumber.Name); }
有兩個方法, Assert.Null 和 Assert.NotNull, 直接傳入期待即可.
測試會Pass的.
集合 Collection Assert
修改一下被測試類, 添加一個集合屬性, 並賦值:
namespace Hospital { public abstract class Worker { public string Name { get; set; } public abstract double TotalReward { get; } public abstract double Hours { get; } public double Salary => TotalReward / Hours; public List<string> Tools { get; set; } } public class Plumber : Worker { public Plumber() { Tools = new List<string>() { "螺絲刀", "扳子", "鉗子" }; } public override double TotalReward => 200; public override double Hours => 3; } }
測試是否包含某個元素, Assert.Contains():
[Fact] public void HaveScrewdriver() { var plumber = new Plumber(); Assert.Contains("螺絲刀", plumber.Tools); }
Build, Run Test, 結果Pass.
修改一下名字, 讓其Fail:

這個失敗信息還是很詳細的.
相應的還有一個Assert.DoesNotContain()方法, 測試集合是否不包含某個元素.
[Fact] public void NotHaveKeyboard() { var plumber = new Plumber(); Assert.DoesNotContain("鍵盤", plumber.Tools); }
這個test也會pass.
Predicate:
測試一下集合中是否包含符合某個條件的元素:
[Fact] public void HaveAtLeastOneScrewdriver() { var plumber = new Plumber(); Assert.Contains(plumber.Tools, t => t.Contains("螺絲刀")); }
使用的是Assert.Contains的一個overload方法, 它的第一個參數是集合, 第二個參數是Predicate.
Build, Run Test, 會Pass的.
比較集合相等:
添加Test:
[Fact] public void HaveAllTools() { var plumber = new Plumber(); var expectedTools = new [] { "螺絲刀", "扳子", "鉗子" }; Assert.Equal(expectedTools, plumber.Tools); }
注意, Plumber的tools類型是List, 這里的expectedTools類型是array.
這個test 仍然會Pass.
如果修改一個元素, 那么測試會Fail, 信息如下:

Assert針對集合的每個元素:
如果想對集合的每個元素進行Assert, 當然可以通過循環來Assert了, 但是更好的寫法是調用Assert.All()方法:
[Fact] public void HaveNoEmptyDefaultTools() { var plumber = new Plumber(); Assert.All(plumber.Tools, t => Assert.False(string.IsNullOrEmpty(t))); }
這個測試會Pass.
如果在被測試類的Tools屬性添加一個空字符串, 那么失敗信息會是:

這里寫到, 4個元素里面有1個沒有pass.
針對Object類型的Assert
首先再添加一個Programmer類:
public class Programmer : Worker { public override double TotalReward => 1000; public override double Hours => 3.5; }
然后建立一個WorkerFactory:
namespace Hospital { public class WorkerFactory { public Worker Create(string name, bool isProgrammer = false) { if (isProgrammer) { return new Programmer { Name = name }; } return new Plumber { Name = name }; } } }
判斷是否是某個類型 Assert.IsType<Type>(xx):
建立一個測試類 WorkerShould.cs和一個test:
namespace Hospital.Tests { public class WorkerShould { [Fact] public void CreatePlumberByDefault() { var factory = new WorkerFactory(); Worker worker = factory.Create("Nick"); Assert.IsType<Plumber>(worker); } } }
Build, Run Test: 結果Pass.
相應的, 還有一個Assert.IsNotType<Type>(xx)方法.
利用Assert.IsType<Type>(xx)的返回值, 它會返回Type(xx的)的這個實例, 添加個一test:
[Fact] public void CreateProgrammerAndCastReturnedType() { var factory = new WorkerFactory(); Worker worker = factory.Create("Nick", isProgrammer: true); Programmer programmer = Assert.IsType<Programmer>(worker); Assert.Equal("Nick", programmer.Name); }
Build, Run Tests: 結果Pass.
Assert針對父類:
寫這樣一個test, 創建的是一個promgrammer, Assert的類型是它的父類Worker:
[Fact] public void CreateProgrammer_AssertAssignableTypes() { var factory = new WorkerFactory(); Worker worker = factory.Create("Nick", isProgrammer: true); Assert.IsType<Worker>(worker); }
這個會Fail:

這時就應該使用這個方法, Assert.IsAssignableFrom<祖先類>(xx):
[Fact] public void CreateProgrammer_AssertAssignableTypes() { var factory = new WorkerFactory(); Worker worker = factory.Create("Nick", isProgrammer: true); Assert.IsAssignableFrom<Worker>(worker); }
Build, Run Tests: Pass.
Assert針對對象的實例
判斷兩個引用是否指向不同的實例 Assert.NotSame(a, b):
[Fact] public void CreateSeperateInstances() { var factory = new WorkerFactory(); var p1 = factory.Create("Nick"); var p2 = factory.Create("Nick"); Assert.NotSame(p1, p2); }
由工廠創建的兩個對象是不同的實例, 所以這個test會Pass.
相應的還有個Assert.Same(a, b) 方法.
Assert 異常
為WorkFactory先添加一個異常處理:
namespace Hospital { public class WorkerFactory { public Worker Create(string name, bool isProgrammer = false) { if (name == null) { throw new ArgumentNullException(nameof(name)); } if (isProgrammer) { return new Programmer { Name = name }; } return new Plumber { Name = name }; } } }
如果在test執行代碼時拋出異常的話, 那么test會直接fail掉.
所以應該使用Assert.Throws<ArgumentNullException>(...)方法來Assert是否拋出了特定類型的異常.
添加一個test:
[Fact] public void NotAllowNullName() { var factory = new WorkerFactory();
// var p = factory.Create(null); // 這個會失敗 Assert.Throws<ArgumentNullException>(() => factory.Create(null)); }
注意不要直接運行會拋出異常的代碼. 應該在Assert.Throws<ET>()的方法里添加lambda表達式來調用方法.
這樣的話就會pass.
如果被測試代碼沒有拋出異常的話, 那么test會fail的. 把拋異常代碼注釋掉之后再Run:

更具體的, 還可以指定參數的名稱:
[Fact] public void NotAllowNullName() { var factory = new WorkerFactory(); // Assert.Throws<ArgumentNullException>(() => factory.Create(null)); Assert.Throws<ArgumentNullException>("name", () => factory.Create(null)); }
這里就是說異常里應該有一個叫name的參數.
Run: Pass.
如果把"name"改成"isProgrammer", 那么這個test會fail:

利用Assert.Throws<ET>()的返回結果, 其返回結果就是這個拋出的異常實例.
[Fact] public void NotAllowNullNameAndUseReturnedException() { var factory = new WorkerFactory(); ArgumentNullException ex = Assert.Throws<ArgumentNullException>(() => factory.Create(null)); Assert.Equal("name", ex.ParamName); }
Assert Events 是否發生(Raised)
回到之前的Patient類, 添加如下代碼:
public void Sleep() { OnPatientSlept(); } public event EventHandler<EventArgs> PatientSlept; protected virtual void OnPatientSlept() { PatientSlept?.Invoke(this, EventArgs.Empty); }
然后回到PatientShould.cs添加test:
[Fact] public void RaiseSleptEvent() { var p = new Patient(); Assert.Raises<EventArgs>( handler => p.PatientSlept += handler, handler => p.PatientSlept -= handler, () => p.Sleep()); }
Assert.Raises<T>()第一個參數是附加handler的Action, 第二個參數是分離handler的Action, 第三個Action是觸發event的代碼.
Build, Run Test: Pass.
如果注釋掉Patient類里Sleep()方法內部那行代碼, 那么test會fail:

針對INotifyPropertyChanged的特殊Assert:
修改Patient代碼:
namespace Hospital { public class Patient: INotifyPropertyChanged { public Patient() { IsNew = true; _bloodSugar = 5.0f; } public string FirstName { get; set; } public string LastName { get; set; } public string FullName => $"{FirstName} {LastName}"; public int HeartBeatRate { get; set; } public bool IsNew { get; set; } private float _bloodSugar; public float BloodSugar { get => _bloodSugar; set => _bloodSugar = value; } public void HaveDinner() { var random = new Random(); _bloodSugar += (float)random.Next(1, 1000) / 1000; OnPropertyChanged(nameof(BloodSugar)); } public void IncreaseHeartBeatRate() { HeartBeatRate = CalculateHeartBeatRate() + 2; } private int CalculateHeartBeatRate() { var random = new Random(); return random.Next(1, 100); } public void Sleep() { OnPatientSlept(); } public event EventHandler<EventArgs> PatientSlept; protected virtual void OnPatientSlept() { PatientSlept?.Invoke(this, EventArgs.Empty); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
添加一個Test:
[Fact] public void RaisePropertyChangedEvent() { var p = new Patient(); Assert.PropertyChanged(p, "BloodSugar", () => p.HaveDinner()); }
針對INotifyPropertyChanged, 可以使用Assert.PropertyChanged(..) 這個專用的方法來斷定PropertyChanged的Event是否被觸發了.
Build, Run Tests: Pass.
到目前為止, 介紹的都是入門級的內容.
接下來要介紹的是稍微進階一點的內容了.

