自己造轮子是一件苦差事。 现在,您可以专注于业务开发,仅需集成 ⭐️Furion⭐️ 即可。
Skip to main content

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 中测试 章节。