36.1 单元测试
视频教程
【单元测试视频教程】
36.1.1 关于单元测试
引用自百度百科:
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如 C 语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
36.1.2 单元测试好处
- 消灭低级错误
基本的单元测试,可以在系统测试之前,把大部分比较低级的错误都消灭掉,减少系统测试过程中的问题,这样也就减少了系统测试中定位和解决问题的时间成本了。
- 找出潜在的 bug
某些类型的 bug,靠系统测试是很难找到的。例如一些代码分支,平时 99%的场景基本上都走不到,但一旦走到了,如果没有提前测试好,那么可能就是一个灾难。
- 上线前的保证
加了新代码,上线前跑一把单元测试,都通过,说明代码可能没有影响到之前的逻辑,这样上线也比较放心。如果之前的单元测试跑不过,那么很有可能新的代码有潜在的问题,赶紧修复去吧。
- 重构代码的机会
写单元测试的过程中,你可能会顺手把一些 code 重构了,为什么?举例,一些长得非常像的代码,如果每次都要写一堆测试代码去测同样的 code,你会不会抓狂?不测吧,覆盖率又上不去,于是我就会想方设法把待测试的 code 改得尽量的精简,重复代码减少,这样覆盖率上去了,测试也好测了,代码也简洁了。如果没有单元测试和覆盖率的要求的话,坦白说可能一来自己不会发现这些重复的 code,另一方面即使发现了,可能也没有太大的动力去改进。
另外,由于单元测试中,你需要尝试去覆盖一些异常分支,这是系统测试常常走不到的地方,于是就会引起你的一些思考,例如这个异常分支是否真的需要?是否真的会发生?对于一些实际上绝对不会出错的函数,那么我觉得可能异常分支是没必要存在的。
- 重新 review 代码的机会
写 UT 的过程中,我总是会好好看哪些代码执行到了,哪些代码没有执行到,这其实也是一个 review 自己代码的机会,有些时候,并不是 UT 本身帮我找到 bug,而是回头 review 自己代码的时候发现的。
36.1.3 单元测试类型
- 基于 API 接口测试(白盒 + 浅度黑盒测试)
- 基于项目代码测试(深度黑盒测试)
36.1.4 主流的单元测试库
xUnit
(最流行的库,推荐)NUnit
MSTest
在本章节,Furion
框架使用 xUnit
库进行单元测试。
36.1.2 第一个例子
36.1.2.1 创建 xUnit
单元测试项目
36.1.2.2 第一个测试方法
using Xunit;
namespace TestProject1
{
public class UnitTest1
{
[Fact]
public void Test1()
{
Assert.Equal(2, 1 + 1);
}
}
}
单元测试实际上是通过普通的类的方法进行模块功能测试,具体测试则是标记了 [Fact]
特性的方法,在方法中使用 Assert
类提供的静态方法进行 断言
,断言
成功,则测试通过,否则测试不通过。
36.1.2.3 运行测试
在单元测试项目中 右键
选择 运行测试
并打开 测试资源管理器
即可查看测试结果。
36.1.2.4 多个测试方法测试
36.1.2.5 重复/回归测试
后续添加更多测试方法只需在 测试资源管理器
点击 在视图中运行所有测试
播放按钮即可,如下图
36.1.3 集成 Furion
强大功能
Furion
是跨平台、跨项目的开发框架,支持任意项目类型,包括单元测试项目。
36.1.3.1 安装 Furion
包
打开 Nuget
程序包控制台,安装 Furion
包
36.1.3.2 添加 Startup.cs
类
在单元测试项目根目录下添加 Startup.cs
类,并写下以下代码:
using Furion;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
// 配置启动类类型,第一个参数是 Startup 类完整限定名,第二个参数是当前项目程序集名称
[assembly: TestFramework("TestProject1.Startup", "TestProject1")]
namespace TestProject1
{
/// <summary>
/// 单元测试启动类
/// </summary>
/// <remarks>在这里可以使用 Furion 几乎所有功能</remarks>
public sealed class Startup : XunitTestFramework
{
public Startup(IMessageSink messageSink) : base(messageSink)
{
// 初始化 IServiceCollection 对象
var services = Inject.Create();
// 在这里可以和 .NET Core 一样注册服务了!!!!!!!!!!!!!!
// 构建 ServiceProvider 对象
services.Build();
}
}
}
特别注意
以下代码是必须的!,TestFramework
第一个参数是 类完全限定名(含命名空间)
,第二个参数是 单元测试程序集名称
[assembly: TestFramework("TestProject1.Startup", "TestProject1")]
36.1.3.3 使用 Furion
完整功能
Furion
是跨平台、跨项目的开发框架,下面在单元测试中演示 远程请求
并请求 https://www.baidu.com
数据,并测试是否请求成功。
- 在
Startup.cs
中注册远程请求服务
public Startup(IMessageSink messageSink) : base(messageSink)
{
// 初始化 IServiceCollection 对象
var services = Inject.Create();
// 在这里可以和 .NET Core 一样注册服务了!!!!!!!!!!!!!!
services.AddRemoteRequest();
// 构建 ServiceProvider 对象
services.Build();
}
- 编写测试方法
[Fact]
public async Task 测试请求百度()
{
var rep = await "https://www.baidu.com".GetAsync();
Assert.True(rep.IsSuccessStatusCode);
}
- 查看测试结果
很神奇吧!Furion
支持任何项目类型,任何平台使用。
36.1.4 带参数的测试方法
上面例子中,测试方法都是没有参数的,有时候需要同一个方法输入多个不同的值进行测试,这时候就需要用到 [Theory]
和 [InlineData]
特性了。
如,下面测试两个数的和是 奇数
,测试代码如下:
[Theory]
[InlineData(1, 2)]
[InlineData(3, 4)]
[InlineData(5, 7)]
public void 带参数测试(int i, int j)
{
Assert.NotEqual(0, (i + j) % 2);
}
测试结果:
36.1.5 如何进行依赖注入
有些时候,我们需要测试某接口,或者进行依赖注入方式解析服务并调用,这时候就需要用到 App.GetService<>()
静态类方式了,如:
36.1.5.1 编写一个 ICalcService
接口及实现类
namespace TestProject1.Services
{
public interface ICalcService
{
int Plus(int i, int j);
}
public class CalcService : ICalcService, ITransient
{
public int Plus(int i, int j)
{
return i + j;
}
}
}
36.1.5.2 在测试类中调用
using Furion;
using TestProject1.Services;
using Xunit;
namespace TestProject1
{
public class UnitTest1
{
private readonly ICalcService _calcService;
/// <summary>
/// 这里不能通过构造函数注入,而是采用 App.GetService<> 方式
/// </summary>
public UnitTest1()
{
_calcService = App.GetService<ICalcService>();
}
[Fact]
public void 测试两个数的和()
{
Assert.Equal(3, _calcService.Plus(1, 2));
}
}
}
36.1.5.3 输出日志
如果在单元测试中想输出日志,只需要在构造函数注入 ITestOutputHelper
即可,如:
using Xunit;
using Xunit.Abstractions;
namespace TestProject1
{
public class UnitTest1
{
private readonly ITestOutputHelper Output;
public UnitTest1(ITestOutputHelper tempOutput)
{
Output = tempOutput;
}
[Fact]
public void Test_String_Equal()
{
Output.WriteLine("哈哈哈哈,我是 Furion");
Assert.NotEqual("Furion", "Fur");
}
}
}
36.1.5.4 关于依赖注入作用域释放问题
有时候我们需要对 非托管资源
或需要手动释放的对象进行服务解析并测试,这时候需要构建作用域进行测试,如:
// 支持异步方法测试
[Fact]
public void 测试数据库()
{
Scoped.Create((f,s) => {
var otherService = s.ServiceProvider.GetService<IOtherService>();
var repository = Db.GetRepository<Person>(s.ServiceProvider);
var isTrue = repository.Any(u => u.Id > 10);
Assert.True(isTrue);
});
}
36.1.5.5 测试释放资源
有时候,我们需要测试成功后释放一些不能及时释放的对象,这时,只需要实现 IDisposable
接口即可:
using System;
using Xunit;
namespace TestProject1
{
public class UnitTest1 : IDisposable
{
[Fact]
public void Test_String_Equal()
{
Assert.NotEqual("Furion", "Fur");
}
public void Dispose()
{
// 释放你的对象
}
}
}
36.1.6 Web
集成测试
有时候,我们需要在没有 IIS
服务器或任何外部事物的情况下测试完整的 Web
应用程序,这时候,我们只需要执行以下步骤即可:
- 单元测试添加
Web
启动层引用 - 单元测试层安装
Microsoft.AspNetCore.TestHost
包 - 编写测试代码
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using WebApplication1;
using Xunit;
namespace TestProject1
{
public class UnitTest1
{
[Fact]
public async Task Test_Web()
{
// 创建一个测试服务
using var testServer = new TestServer(WebHost.CreateDefaultBuilder()
.Inject()
.UseStartup<Startup>()); // 这里的 Startup 就是你 Web 层的 Startup
// 创建一个 HttpClient 客户端
using var httpClient = testServer.CreateClient();
// 测试 Api
var result = await httpClient.GetStringAsync("/api/user/1");
Assert.AreEqual("Furion", result.Name);
}
}
}
小知识
这种方式的好处就是无需在启动中调用 Inject.Create()
初始化,每一个测试方法都有独立的生命周期,不会污染全局。
36.1.7 Assert
断言
Assert
是单元测试判定成功的依据,通常第一个参数为 期望值
,第二个参数为 实际值
,对比这两个值是否一致即可判断成功与否。详细的 Assert
静态方法可查阅官方库 Assert 方法
36.1.8 反馈与建议
与我们交流
给 Furion 提 Issue。
了解更多
想了解更多 单元测试
知识可查阅 在 .NET 中测试 章节。