什么是单元测试
定义
单元测试是编写额外代码(称为测试代码)的想法,目的是尽可能地测试最小的生产代码块,以确保这些代码块没有错误。
代码通常设计为类或对象。术语“类”和术语“对象”通常可互换使用。类由包含构造函数,方法和属性的变量和函数组成,但它们也可能包含子类。
对于代码中的每个对象,应该对该对象进行单元测试。对于对象中的每个方法,相关单元测试应该有一个测试。实际上,应该存在一个测试,确保代码的每一行都按预期运行。
(可能是)最佳实践/最佳做法和指南
- 测试每个单元测试类的一个对象或类。
- 在测试的类之后命名您的测试类。
- 每个测试功能执行一次测试。
- 单元测试应在构建和持续集成(CI)系统上运行。
- 单元测试不应该以任何方式改变系统。
不要连接文件,数据库,注册表,网络等…这样做的测试是功能测试,而不是单元测试。 - 使测试功能名称自我记录。
- 尽可能以最简单的方式进行测试。
- 接受培训并不断学习单元测试。
存在许多测试框架、模拟框架、包装器(例如System Wrapper)和封装问题,你会发现许多关于最佳实践的意见,但你应该知道意见的每一方以及为什么这些意见存在。
如何进行单元测试
以下使用 MSTest、xUnit以及Nunit框架实现面向C#的单元测试。
系统必备
- 安装.NET Core 2.1 SDK 或更高版本
使用MSTest进行单元测试
- 命令行
REM 创建并切换到名叫unit-testing-using-mstest的解决方案目录 mkdir ./unit-testing-using-mstest cd ./unit-testing-using-mstest REM 创建新的解决方案,用于管理类库项目和单元测试项目 dotnet new sln REM 创建并切换到名叫PositiveService的源项目目录 mkdir ./PositiveService cd PositiveService REM 创建源项目并重命名源项目中的类库文件Class1.cs dotnet new classlib ren Class1.cs PositiveService.cs REM 修改PositiveService.cs代码(见2.PositiveService.cs代码) REM 切换到unit-testing-using-mstest的解决方案目录 cd .. REM 向解决方案中添加源(类库项目)项目 dotnet sln add ./PositiveService/PositiveService.csproj
- 修改PositiveService.cs代码
using System; namespace Positive.Service { public class PositiveService { public bool IsPositive(int candidate) { throw new NotImplementedException("Please create a test first"); } } }
- 目录结构
/unit-testing-using-mstest
unit-testing-using-mstest.sln
/PositiveService
Source Files
PositiveService.csproj
创建测试项目
- 命令行
REM 切换到名叫unit-testing-using-mstest的目录 cd ./unit-testing-using-mstest REM 创建并切换到名叫PositiveService.Tests的测试库的测试项目目录 mkdir ./PositiveService.Tests REM 创建名叫PositiveService.Tests的测试项目 dotnet new mstest REM 将PositiveService类库作为依赖项添加到项目中 dotnet add reference ../PositiveService/PositiveService.csproj REM 切换到名叫unit-testing-using-mstest的目录 cd .. REM 向源项目解决方案添加测试项目 dotnet sln add ./PositiveServiceTests/PositiveServiceTests.csproj
- 查看PositiveServiceTest.csproj配置文件
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.1.18" />
<PackageReference Include="MSTest.TestFramework" Version="1.1.18" />
</ItemGroup>
- 目录结构
/unit-testing-using-mstest
unit-testing-using-mstest.sln
/PositiveService
Source Files
PositiveService.csproj
/PositiveService.Tests
Test Source Files
PositiveServiceTests.csproj
创建第一个测试
- 命令行
REM 切换到新建的名叫PositiveService.Tests的测试项目目录 cd ./PositiveService.Tests REM 重命名UnitTest1.cs为PositiveServiceTests ren UnitTest1.cs PositiveServiceTests.cs REM 修改PositiveServiceTest.cs代码(见下方的修改代码) REM 生成测试和类库,之后运行测试 dotnet test REM 测试失败,修改PositiveService.cs代码(见下方的修改代码) REM 生成测试和类库,之后运行测试 dotnet test REM 测试成功
- 修改PositiveServiceTest.cs内容
using Microsoft.VisualStudio.TestTools.UnitTesting; using Positive.Service; namespace Positive.UnitTests.Service { public class PositiveService_IsPositiveShould { private readonly PositiveService _positiveService; public PositiveService_IsPositiveShould() { _positiveService = new PositiveService(); } [Fact] public void ReturnFalseGivenValueOfn1() { var result = _positiveService.IsPositive(-1); Assert.False(result, "1 should not be prime"); } } }
- 修改PositiveService.cs类库代码,使测试通过
public bool IsPositive(int candidate) { if (candidate == 0) { return false; } throw new NotImplementedException("Please create a test first"); }
添加更多功能
- 命令行
REM 切换到PositiveService.Tests的测试项目目录 cd ./PositiveService.Tests REM 修改测试代码(见下方修改测试代码) REM 运行测试 dotnet test
- 修改PositiveServiceTest.cs测试代码
[DataTestMethod] [DataRow(-1)] [DataRow(0)] [DataRow(1)] public void ReturnFalseGivenValuesLessThan1(int value) { var result = _positiveService.IsPositive(value); Assert.False(result, $"{value} should not be positive"); }
注意:
- 使用MSUnit属性[Fact]需要编写重复测试;使用MSUnit属性[DataTestMethod]测试套件,避免重复编写
- MSUnit属性[DataTestMethod] 执行相同代码,但具有不同输入参数的测试套件
- MSUnit属性[DataRow] 指定这些输入的值
使用xUnit进行单元测试
创建源项目
- 命令行
REM 创建并切换到名叫unit-testing-using-dotnet-test的解决方案目录 mkdir ./unit-testing-using-dotnet-test cd ./unit-testing-using-dotnet-test REM 创建新的解决方案,用于管理类库项目和单元测试项目 dotnet new sln REM 创建并切换到名叫PositiveService的源项目目录 mkdir ./PositiveService cd PositiveService REM 创建源项目并重命名源项目中的类库文件Class1.cs dotnet new classlib ren Class1.cs PositiveService.cs REM 修改PositiveService.cs代码(见2.PositiveService.cs代码) REM 切换到unit-testing-using-dotnet-test的解决方案目录 cd .. REM 向解决方案中添加源(类库项目)项目 dotnet sln add ./PositiveService/PositiveService.csproj
- 修改PositiveService.cs代码
using System; namespace Positive.Service { public class PositiveService { public bool IsPositive(int candidate) { throw new NotImplementedException("Please create a test first"); } } }
- 目录结构
/unit-testing-using-dotnet-test
unit-testing-using-dotnet-test.sln
/PositiveService
Source Files
PositiveService.csproj
创建测试项目
- 命令行
REM 切换到名叫unit-testing-using-dotnet-test的目录 cd ./unit-testing-using-dotnet-test REM 创建并切换到名叫PositiveService.Tests的测试库的测试项目目录 mkdir ./PositiveService.Tests REM 创建名叫PositiveService.Tests的测试项目 dotnet new xunit REM 将PositiveService类库作为依赖项添加到项目中 dotnet add reference ../PositiveService/PositiveService.csproj REM 切换到名叫unit-testing-using-dotnet-test的目录 cd .. REM 向源项目解决方案添加测试项目 dotnet sln add ./PositiveServiceTests/PositiveServiceTests.csproj
- 查看PositiveServiceTest.csproj配置文件
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup>
- 目录结构
/unit-testing-using-dotnet-test
unit-testing-using-dotnet-test.sln
/PositiveService
Source Files
PositiveService.csproj
/PositiveService.Tests
Test Source Files
PositiveServiceTests.csproj
创建第一个测试
- 命令行
REM 切换到新建的名叫PositiveService.Tests的测试项目目录 cd ./PositiveService.Tests REM 重命名UnitTest1.cs为PositiveServiceTests ren UnitTest1.cs PositiveServiceTests.cs REM 修改PositiveServiceTest.cs代码(见下方的修改代码) REM 生成测试和类库,之后运行测试 dotnet test REM 测试失败,修改PositiveService.cs代码(见下方的修改代码) REM 生成测试和类库,之后运行测试 dotnet test REM 测试成功
- 修改PositiveServiceTest.cs内容
using Xunit; using PositiveService; namespace PositiveService.UnitTests { public class PositiveService_IsPositiveShould { private readonly PositiveService _positiveService; public PositiveService_IsPositiveShould() { _positiveService = new PositiveService(); } [Fact] public void ReturnFalseGivenValueOfn1() { var result = _positiveService.IsPositive(-1); Assert.False(result, "-1 should not be positive"); } } }
- 修改PositiveService.cs类库代码,使测试通过
public bool IsPositive(int candidate) { if (candidate == -1) { return false; } throw new NotImplementedException("Please create a test first"); }
添加更多功能
- 命令行
REM 切换到PositiveService.Tests的测试项目目录 cd ./PositiveService.Tests REM 修改测试代码(见下方修改测试代码) REM 运行测试 dotnet test
- 修改PositiveServiceTest.cs测试代码
[Theory] [InlineData(-1)] [InlineData(0)] [InlineData(1)] public void ReturnFalseGivenValuesLessThan1(int value) { var result = _positiveService.IsPositive(value); Assert.False(result, $"{value} should not be positive"); }
注意:
- 使用xUnit属性[Fact]需要编写重复测试;使用xUnit属性[Theory]测试套件,避免重复编写
- xUnit属性[Theory] 执行相同代码,但具有不同输入参数的测试套件
- xUnit属性[InlineData] 指定这些输入的值
使用NUnit进行单元测试
- 命令行
REM 创建并切换到名叫unit-testing-using-nunit的解决方案目录 mkdir ./unit-testing-using-nunit cd ./unit-testing-using-nunit REM 创建新的解决方案,用于管理类库项目和单元测试项目 dotnet new sln REM 创建并切换到名叫PositiveService的源项目目录 mkdir ./PositiveService cd PositiveService REM 创建源项目并重命名源项目中的类库文件Class1.cs dotnet new classlib ren Class1.cs PositiveService.cs REM 修改PositiveService.cs代码(见2.PositiveService.cs代码) REM 切换到unit-testing-using-nunit的解决方案目录 cd .. REM 向解决方案中添加源(类库项目)项目 dotnet sln add ./PositiveService/PositiveService.csproj
- 修改PositiveService.cs代码
using System; namespace Positive.Services { public class PositiveService { public bool IsPositive(int candidate) { throw new NotImplementedException("Please create a test first"); } } }
- 目录结构
/unit-testing-using-nunit
unit-testing-using-nunit.sln
/PositiveService
Source Files
PositiveService.csproj
创建测试项目
- 命令行
REM 切换到名叫unit-testing-using-nunit的目录 cd ./unit-testing-using-nunit REM 创建并切换到名叫PositiveService.Tests的测试库的测试项目目录 mkdir ./PositiveService.Tests REM 创建名叫PositiveService.Tests的测试项目 dotnet new mstest REM 将PositiveService类库作为依赖项添加到项目中 dotnet add reference ../PositiveService/PositiveService.csproj REM 切换到名叫unit-testing-using-nunit的目录 cd .. REM 向源项目解决方案添加测试项目 dotnet sln add ./PositiveServiceTests/PositiveServiceTests.csproj
- 查看PositiveServiceTest.csproj配置文件
<ItemGroup>
<PackageReference Include="nunit" Version="3.10.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
</ItemGroup>
- 目录结构
/unit-testing-using-nunit
unit-testing-using-nunit.sln
/PositiveService
Source Files
PositiveService.csproj
/PositiveService.Tests
Test Source Files
PositiveServiceTests.csproj
创建第一个测试
- 命令行
REM 切换到新建的名叫PositiveService.Tests的测试项目目录 cd ./PositiveService.Tests REM 重命名UnitTest1.cs为PositiveServiceTests ren UnitTest1.cs PositiveServiceTests.cs REM 修改PositiveServiceTest.cs代码(见下方的修改代码) REM 生成测试和类库,之后运行测试 dotnet test REM 测试失败,修改PositiveService.cs代码(见下方的修改代码) REM 生成测试和类库,之后运行测试 dotnet test REM 测试成功
- 修改PositiveServiceTest.cs内容
using NUnit.Framework; using Prime.Services; namespace Positive.UnitTests.Services { [TestFixture] public class PositiveService_IsPositiveShould { private readonly PositiveService _positiveService; public PositiveService_IsPositiveShould() { _positiveService = new PositiveService(); } [Test] public void ReturnFalseGivenValueOfn1() { var result = _positiveService.IsPositive(-1); Assert.False(result, "1 should not be prime"); } } }
- 修改PositiveService.cs类库代码,使测试通过
public bool IsPositive(int candidate) { if (candidate == 0) { return false; } throw new NotImplementedException("Please create a test first"); }
注意
- [TestFixture] 指示包含单元测试的类
- [Test] 指示方法是测试方法
添加更多功能
- 命令行
REM 切换到PositiveService.Tests的测试项目目录 cd ./PositiveService.Tests REM 修改测试代码(见下方修改测试代码) REM 运行测试 dotnet test
- 修改PositiveServiceTest.cs测试代码
[TestCase(-1)] [TestCase(0)] [TestCase(1)] public void ReturnFalseGivenValuesLessThan1(int value) { var result = _positiveService.IsPositive(value); Assert.False(result, $"{value} should not be positive"); }
注意:
- 使用NUnit属性[Test]需要编写重复测试;使用NUnit属性[TestCase]测试套件,避免重复编写
- NUnit属性[TestCase] 创建一套可执行相同代码但具有不同输入参数的测试
MSTest、xUnit以及NUnit方案对比
(内容待更新)
为何进行单元测试
优点
- 比执行功能测试节省时间
功能测试费用高。 - 防止回归
回归缺陷是在对应用程序进行更改时引入的缺陷。
可执行文档
每个测试应能够清楚地解释给定输入的预期输出。
减少耦合代码
为代码编写测试会自然地解耦代码。