基础
意义
- 可靠:比人工测试要快要可靠
- 可迭代:测试代码可根据实际生产代码需要进行迭代
可自动化:可在任何时间任何环境(具备相关运行环境)进行测试且可频繁重复进行测试。
分类
纵轴代码测试的深度,横轴代表测试的广度(覆盖度)。单元测试(Unit Test):可以测试一个类或者一个类的某个功能,具有很好的深度,但对于整个应用或者功能没有很好的覆盖面。
- 集成测试(Integration Test):没有单元测试那么细致,但具有相对较好的测试覆盖面。例如集成测试可用于测试功能的组合、数据库或文件系统的外部资源等。
- 皮下测试(Subcutaneous Test):这种测试作用于UI层的下面一层,对整个引用有着很好的覆盖率,但是深度欠佳。
-
测试三阶段AAA
Arrange: 这里做一些先决的设定,例如创建对象实例、数据、输入等。
- Act:在这里执行生产代码并返回结果,例如调用方法或设置属性。
- Assert:在这里检查结果,测试通过或失败。
xUnit.NET
基本概念
https://github.com/xunit/xunit
xUnit是一个测试框架,可以针对.NET或.NET Core项目进行测试。测试项目需引用被测项目和xUnit库。测试用例编写好后,用Test Runner来运行测试。目前可用的Test Runner包括vs自带的Test Explorer或者dotnet test命令行,以及第三方工具(resharper)等。各参数的含义:https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-test
xUnit支持的平台:.net full, uwp,xamarin
示例:dotnet test -用于执行单元测试的.NET测试驱动程序。
dotnet test [<PROJECT> | <SOLUTION> | <DIRECTORY> | <DLL>]
[-a|--test-adapter-path <ADAPTER_PATH>] [--blame] [--blame-crash]
[--blame-crash-dump-type <DUMP_TYPE>] [--blame-crash-collect-always]
[--blame-hang] [--blame-hang-dump-type <DUMP_TYPE>]
[--blame-hang-timeout <TIMESPAN>]
[-c|--configuration <CONFIGURATION>]
[--collect <DATA_COLLECTOR_NAME>]
[-d|--diag <LOG_FILE>] [-f|--framework <FRAMEWORK>]
[--filter <EXPRESSION>] [--interactive]
[-l|--logger <LOGGER>] [--no-build]
[--nologo] [--no-restore] [-o|--output <OUTPUT_DIRECTORY>]
[-r|--results-directory <RESULTS_DIR>] [--runtime <RUNTIME_IDENTIFIER>]
[-s|--settings <SETTINGS_FILE>] [-t|--list-tests]
[-v|--verbosity <LEVEL>] [[--] <RunSettings arguments>]
dotnet test -h|--help
``` using Microsoft.VisualStudio.TestTools.UnitTesting;dotnet test 运行当前目录所含项目中的测试
dotnet test ~/projects/test1/test1.csproj 运行test1项目中的测试
dotnet test ~/projects/test1/bin/debug/test1.dll 运行test1.dll程序集中的测试
dotnet test --logger trx 在当前目录运行项目中的测试,并以trx格式生成测试结果文件
dotnet test --logger "console;verbosity=detailed" 记录详细的测试结果日志到控制台上
dotnet test --collect:"Code Coverage" 这个需要安装Coverlet并进行一定的配置,默认配置会生成名字为Code Coverage的.coverage文件,用于收集测试代码覆盖率。
dotnet test --blame 在当前目录下的项目中运行测试,并报告在测试主机发生故障时正在进行的测试
dotnet test --filter Method 运行FullyQualifiedName包含Method的测试
dotnet test --filter Name~TestMethod1 运行名称包含TestMethod1的测试
dotnet test --filter FullyQualifiedName!=MSTestNamespace.UnitTest1.TestMethod1 运行除 MSTestNamespace.UnitTest1.TestMethod1 之外的其他所有测试。
dotnet test --filter Category=CategoryA 运行含 [TestCategory("CategoryA")] 批注的测试。
dotnet test --filter "FullyQualifiedName=MyNamespace.MyTestsClass<ParameterType1%2CParameterType2>.MyTestMethod" 对于包含泛型类型参数的逗号的 FullyQualifiedName 值,请使用 %2C 来转义逗号。
namespace MSTestNamespace { [TestClass] public class UnitTest1 { [TestMethod, Priority(1), TestCategory(“CategoryA”)] public void TestMethod1() { }
[TestMethod, Priority(2)]
public void TestMethod2()
{
}
}
}
<a name="FUFK7"></a>
## 安装配置xUnit.NET
- 创建一个albertxunit类库项目(dotnet new classlib -n albertxunit)
- 创建xUnit Test项目(albertxunit.Test) (dotnet new xunit -n test_albertxunit)
(dotnet add reference ../albertxunit/albertxunit.csproj)<br />(dotnet sln add test_albertxunit\test_albertxunit.csproj)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/957395/1647850566235-a17d6c9e-3452-4abe-8a77-f306b04b61f2.png#clientId=u54e1043e-0ed3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=711&id=ub959b8fa&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1422&originWidth=2120&originalType=binary&ratio=1&rotation=0&showTitle=false&size=174815&status=done&style=none&taskId=ud0e64830-9f92-432a-ae19-27af2edb6b6&title=&width=1060)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/957395/1647850769870-8dcf225c-a5fd-4cc9-ad07-4dcdcdea2f24.png#clientId=u54e1043e-0ed3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=251&id=u1dfc3c29&margin=%5Bobject%20Object%5D&name=image.png&originHeight=502&originWidth=692&originalType=binary&ratio=1&rotation=0&showTitle=false&size=46775&status=done&style=none&taskId=u9ba17501-815c-408a-b037-b68c92fffe0&title=&width=346)<br />创建完成后,默认引用了这四个Packages,其中coverlet.collector是用于收集代码测试覆盖率的。(dotnet test --collect:"Code Coverage" 这个需要安装Coverlet并进行一定的配置,默认配置会生成名字为Code Coverage的.coverage文件,用于收集测试代码覆盖率。)
- 引用待测项目,从Test Explorer可以看到待测项目
![image.png](https://cdn.nlark.com/yuque/0/2022/png/957395/1647851141104-c4f43ba2-ebcc-42a3-8b89-ab3beb08022f.png#clientId=u54e1043e-0ed3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=356&id=ub980fab3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=712&originWidth=2108&originalType=binary&ratio=1&rotation=0&showTitle=false&size=120643&status=done&style=none&taskId=u6050e80f-a6cc-4524-9329-80cf4f2113e&title=&width=1054)
<a name="y6tS2"></a>
## Assert
Assert(断言)是基于代码的返回值、对象的最终状态、事件是否发生等情况来评估测试的结果。<br />xUnit提供了以下类型的Assert:
- boolen:True/False
- String:相等/不等, 是否为空,以..开始/结束,是否包含子字符串,匹配正则表达式
- 数值型:相等/不等,是否在某个范围内,浮点的精度
- Collection:内容是否相等,是否包含某个元素,是否包含满足某种条件(predicate)的元素,是否所有的元素都满足某个assert
- Raised events:Custom events,Framework events(例如:PropertyChanged)
- Object Type:是否是某种类型,是否某种类型或继承与某种类型
Tips一般每个Test方法中只有一个assert, 如果一个test里面有多个asserts,只要这些assert都是针对同一个行为的即可。
<a name="k5995"></a>
### 测试案列1-boolen
```csharp
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);
}
}
运行测试,进入到项目目录下,dotnet test运行整个测试
只运行单个方法 dotnet test —filter TestIsNew
生成测试日志 dotnet test —logger trx
使用命令行将详细日志信息打印出来 dotnet test —logger “console;verbosity=detailed” detailed/minimal
生成测试覆盖率文件 dotnet test —collect “Code Coverage”
测试案例2-string
using albertxunit;
using Xunit;
namespace Test_albertxunit
{
public class PatientTest
{
[Fact]
public void TestIsNew()
{
Patient patient = new Patient();
Assert.True(patient.IsNew);
}
[Fact]
public void CalFullName()
{
Patient p = new Patient();
p.FirstName = "albert";
p.LastName = "zhao";
Assert.Equal("albert zhao",p.FullName);
}
//正则表达式,匹配首字母是否是大写
[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);
}
}
}
dotnet build之后,执行dotnet test —filter CalFullName,测试结果
测试案例3:数值
public void HaveDinner()
{
var random = new Random();
_bloodSugar += (float)random.Next(1, 1000) / 100; // 应该是1000
}
[Fact]
public void BloodSugarIncreaseAfterDinner()
{
var p = new Patient();
p.HaveDinner();
// Assert.InRange<float>(p.BloodSugar, 5, 6);
Assert.InRange(p.BloodSugar, 5, 6);
}
//小数点精度三位,precision
[Fact]
public void HaveCorrectSalary()
{
var plumber = new Plumber();
Assert.Equal(66.667, plumber.Salary, precision: 3);
}
测试案例4: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);
}
测试案例5:Collection Assert Contains/NotContains/Equal/All
[Fact]
public void HaveScrewdriver()
{
var plumber = new Plumber();
Assert.Contains("螺丝刀", plumber.Tools);
}
[Fact]
public void NotHaveKeyboard()
{
var plumber = new Plumber();
Assert.DoesNotContain("键盘", plumber.Tools);
}
//两个集合比较
[Fact]
public void HaveAllTools()
{
var plumber = new Plumber();
var expectedTools = new []
{
"螺丝刀",
"扳子",
"钳子"
};
Assert.Equal(expectedTools, plumber.Tools);
}
//集合每个元素比较
[Fact]
public void HaveNoEmptyDefaultTools()
{
var plumber = new Plumber();
Assert.All(plumber.Tools, t => Assert.False(string.IsNullOrEmpty(t)));
}
测试案例6:Object类型
IsType
namespace Hospital.Tests
{
public class WorkerShould
{
[Fact]
public void CreatePlumberByDefault()
{
var factory = new WorkerFactory();
Worker worker = factory.Create("Nick");
Assert.IsType<Plumber>(worker);
}
[Fact]
public void TestIsAssignalbeFrom()
{
var factory = new WorkerFactory();
var p1 = factory.Create("Nike", true);
Assert.IsAssignableFrom<Worker>(p1);
}
[Fact]
public void SameInstance()
{
var factory = new WorkerFactory();
var p1 = factory.Create("Nike");
var p2 = factory.Create("Jack");
Assert.NotSame(p1, p2);
//Assert.Same(p1, p2);
}
}
}Ty
测试案例7:异常
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 };
}
}
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;
}
public class Programmer : Worker
{
public override double TotalReward => 1000;
public override double Hours => 3.5;
}
}
[Fact]
public void NotAllowNullName()
{
var factory = new WorkerFactory(); // var p = factory.Create(null); // 这个会失败
Assert.Throws<ArgumentNullException>(() => factory.Create(null));
}
[Fact]
public void NotAllowNullName()
{
var factory = new WorkerFactory();
// Assert.Throws<ArgumentNullException>(() => factory.Create(null));
Assert.Throws<ArgumentNullException>("name", () => factory.Create(null));
}
[Fact]
public void NotAllowNullNameAndUseReturnedException()
{
var factory = new WorkerFactory();
ArgumentNullException ex = Assert.Throws<ArgumentNullException>(() => factory.Create(null));
Assert.Equal("name", ex.ParamName);
}
测试案例8:Events(Is raised)
public void Sleep()
{
OnPatientSlept();
}
public event EventHandler<EventArgs> PatientSlept;
protected virtual void OnPatientSlept()
{
PatientSlept?.Invoke(this, EventArgs.Empty);
}
[Fact]
public void RaiseSleptEvent()
{
var p = new Patient();
Assert.Raises<EventArgs>(
handler => p.PatientSlept += handler,
handler => p.PatientSlept -= handler,
() => p.Sleep());
}
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));
}
}
}
[Fact]
public void RaisePropertyChangedEvent()
{
var p = new Patient();
Assert.PropertyChanged(p, "BloodSugar", () => p.HaveDinner());
}