使用xUnit為.net core程式進行單元測試(2)
下面有一點點內容是重疊的....
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.
到目前為止, 介紹的都是入門級的內容.
接下來要介紹的是稍微進階一點的內容了.