❤️ 关注 Furion 微信公众号有惊喜哦!
Skip to main content

36.1 单元/集成测试

📝 模块更新日志
  • 问题修复

    •   Furion.Xunit/Furion.Pure.Xunit 单元测试依赖注入单例服务时不是同一实例问题 4.8.5.3 ⏱️2023.01.31 305511e

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.5 第一个例子

36.1.5.1 创建 xUnit 单元测试项目

36.1.5.2 第一个测试方法

using Xunit;

namespace TestProject1
{
public class UnitTest1
{
[Fact]
public void Test1()
{
Assert.Equal(2, 1 + 1);
}
}
}

单元测试实际上是通过普通的类的方法进行模块功能测试,具体测试则是标记了 [Fact] 特性的方法,在方法中使用 Assert 类提供的静态方法进行 断言断言 成功,则测试通过,否则测试不通过。

36.1.5.3 运行测试

在单元测试项目中 右键 选择 运行测试 并打开 测试资源管理器 即可查看测试结果。

36.1.5.4 多个测试方法测试

36.1.5.5 重复/回归测试

后续添加更多测试方法只需在 测试资源管理器 点击 在视图中运行所有测试 播放按钮即可,如下图

36.1.6 集成 Furion 强大功能

Furion 是跨平台、跨项目的开发框架,支持任意项目类型,包括单元测试项目。

36.1.6.1 安装 Furion.Xunit

Furion 纯净版

如果使用的是 Furion.Pure 则安装 Furion.Pure.Xunit 这个拓展包。

打开 NuGet 程序包控制台,安装 Furion.Xunit

特别注意

Furion.Xunit 已经包含 Furion 无需再次安装 Furion

36.1.6.2 添加初始配置类

在单元测试项目根目录下添加 TestProgram.cs 类,并写下以下代码:

TestProgram.cs
using Furion.Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

// 配置启动类类型,第一个参数是 TestProgram 类完整限定名,第二个参数是当前项目程序集名称
[assembly: TestFramework("TestProject1.TestProgram", "TestProject1")]

namespace TestProject1;

/// <summary>
/// 单元测试启动类
/// </summary>
public class TestProgram : TestStartup
{
public TestProgram(IMessageSink messageSink) : base(messageSink)
{
// 初始化 Furion
Serve.RunNative();
}
}
小提示

TestProgram.cs 名称可随意,只需要继承 TestStartup 类即可。但类型必须是 public 公开的

36.1.6.3 使用 Furion 完整功能

Furion 是跨平台、跨项目的开发框架,下面在单元测试中演示 远程请求 并请求 https://www.baidu.com 数据,并测试是否请求成功。

Furion 提供了以下两种方式注册服务:

  • Serve.RunNative 方式(推荐
Serve.RunNative(services =>
{
// 注册远程服务
services.AddRemoteRequest();
});
  • Startup.cs 方式
Startup.cs
using Furion;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace TestProject1;

public class Startup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
// 注册远程服务
services.AddRemoteRequest();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
}
}
  • 编写测试方法
[Fact]
public async Task 测试请求百度()
{
var rep = await "https://www.baidu.com".GetAsync();
Assert.True(rep.IsSuccessStatusCode);
}
  • 查看测试结果

很神奇吧!Furion 支持任何项目类型,任何平台使用。

36.1.7 带参数的测试方法

上面例子中,测试方法都是没有参数的,有时候需要同一个方法输入多个不同的值进行测试,这时候就需要用到 [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.8 如何进行依赖注入

有些时候,我们需要测试某接口,或者进行依赖注入方式解析服务并调用,Furion.Xunit 提供完整的构造函数注入。

36.1.8.1 编写一个 ICalcService 接口及实现类

using Furion.DependencyInjection;

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.8.2 在测试类中调用

using TestProject1.Services;
using Xunit;

namespace TestProject1;

public class UnitTest1
{
private readonly ICalcService _calcService;
public UnitTest1(ICalcService calcService)
{
_calcService = calcService;
}

[Fact]
public void 测试两个数的和()
{
Assert.Equal(3, _calcService.Plus(1, 2));
}
}

36.1.8.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.8.4 关于依赖注入作用域释放

Furion 会在创建单元测试实例时创建一个 IServiceScope 对象,等该实例所有测试案例执行完毕自动调用 Dispose,编写测试的开发者无需关注。

36.1.8.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.8.6 [AssemblyFixture] 特性

有时候我们可能不需要对类进行依赖注册,或者无法通过外部进行注册,这时候可以通过 [AssemblyFixture] 特性实现构造函数注入任何类,如:

有效范围说明

[AssemblyFixture] 方式对整个单元测试类构造函数都有效,如需个别单元测试类有效可使用 IClassFixture<>ICollectionFixture<> + [Collection] 组合方式。

  • 定义需要注入进单元测试构造函数中的类
public class MyAssemblyFixture : IDisposable
{
public static int InstantiationCount;

public MyAssemblyFixture()
{
InstantiationCount++;
}

public void Dispose()
{
// 做一些释放工作
}
}
  • TestProgram.cs 顶部全局注册
TestProgram.cs
using Furion.Xunit;
using TestProject1;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

// 配置启动类类型,第一个参数是 TestProgram 类完整限定名,第二个参数是当前项目程序集名称
[assembly: TestFramework("TestProject1.TestProgram", "TestProject1")]

// 支持多个
[assembly: AssemblyFixture(typeof(MyAssemblyFixture))]
// [assembly: AssemblyFixture(typeof(XXXXFixture))]

namespace TestProject1;

/// <summary>
/// 单元测试启动类
/// </summary>
public class TestProgram : TestStartup
{
public TestProgram(IMessageSink messageSink) : base(messageSink)
{
Serve.Run(silence: true);
}
}
  • 在测试类构造函数注入
using TestProject1.Services;
using Xunit;

namespace TestProject1;

public class UnitTest1
{
private readonly ICalcService _calcService;
private readonly MyAssemblyFixture _fixture;

public UnitTest1(ICalcService calcService
, MyAssemblyFixture fixture)
{
_calcService = calcService;
_fixture = fixture;
}

[Fact]
public void 测试两个数的和()
{
Assert.Equal(3, _calcService.Plus(1, 2));
}

[Fact]
public void EnsureSingleton()
{
Assert.Equal(1, MyAssemblyFixture.InstantiationCount);
}
}

36.1.8.7 IClassFixture<> 单个注入

通过上面 [AssemblyFixture] 方式我们知道此方式对全局的单元测试类都有效,但有时候我们只需要特定单元测试类有效,则可通过 IClassFixture<> 方式,如:

  • 定义需要注入进单元测试构造函数中的类
public class MyClassFixture : IDisposable
{
public static int InstantiationCount;

public MyClassFixture()
{
InstantiationCount++;
}

public void Dispose()
{
// 做一些释放工作
}
}
  • 在测试类构造函数注入
using TestProject1.Services;
using Xunit;

namespace TestProject1;

public class UnitTest1 : IClassFixture<MyClassFixture>
{
private readonly ICalcService _calcService;
private readonly MyAssemblyFixture _fixture;
private readonly MyClassFixture _classFixture;

public UnitTest1(ICalcService calcService
, MyAssemblyFixture fixture
, MyClassFixture classFixture)
{
_calcService = calcService;
_fixture = fixture;
_classFixture = classFixture;
}

[Fact]
public void 测试两个数的和()
{
Assert.Equal(3, _calcService.Plus(1, 2));
}

[Fact]
public void EnsureSingleton()
{
Assert.Equal(1, MyAssemblyFixture.InstantiationCount);
}

[Fact]
public void EnsureClassSingleton()
{
Assert.Equal(1, MyClassFixture.InstantiationCount);
}
}

36.1.8.8 ICollectionFixture<> 多个注入

ICollectionFixture<> 方式和 IClassFixture<> 方式最大的不同就是后者只能配置为单个测试类使用,而 ICollectionFixture<> 则通过 [Collection] 方式配置多个测试类有效,如:

  • 定义需要注入进单元测试构造函数中的类
特别注意

这里区别于 IClassFixture<> 方式,需定义配置器并实现 ICollectionFixture<> 接口。

using Xunit;

namespace TestProject1;

public class MyCollectionFixture : IDisposable
{
public static int InstantiationCount;

public MyCollectionFixture()
{
InstantiationCount++;
}

public void Dispose()
{
// 做一些释放工作
}
}

[CollectionDefinition("MyCollection")]
public class MyCollection : ICollectionFixture<MyCollectionFixture>
{
}
  • 在测试类构造函数注入
using TestProject1.Services;
using Xunit;

namespace TestProject1;

[Collection("MyCollection")]
public class UnitTest1 : IClassFixture<MyClassFixture>
{
private readonly ICalcService _calcService;
private readonly MyAssemblyFixture _fixture;
private readonly MyClassFixture _classFixture;
private readonly MyCollectionFixture _collectionFixture;

public UnitTest1(ICalcService calcService
, MyAssemblyFixture fixture
, MyClassFixture classFixture
, MyCollectionFixture collectionFixture)
{
_calcService = calcService;
_fixture = fixture;
_classFixture = classFixture;
_collectionFixture = collectionFixture;
}

[Fact]
public void 测试两个数的和()
{
Assert.Equal(3, _calcService.Plus(1, 2));
}

[Fact]
public void EnsureSingleton()
{
Assert.Equal(1, MyAssemblyFixture.InstantiationCount);
}

[Fact]
public void EnsureClassSingleton()
{
Assert.Equal(1, MyClassFixture.InstantiationCount);
}

[Fact]
public void EnsureCollectionSingleton()
{
Assert.Equal(1, MyCollectionFixture.InstantiationCount);
}
}

36.1.9 Web 集成测试

Web 集成测试有三种方式,通过这三种方式可以对项目进行全方位的测试,保证部署上线是测试期盼效果。

36.1.9.1 对现有项目进行集成测试

这种方式比较简单,也是最常用的方式,无需部署到服务器直接在本地即可测试,如:

  1. 创建 Xunit 单元测试项目
  1. 添加 Microsoft.AspNetCore.Mvc.Testing 微软提供的集成测试拓展
  1. 添加测试项目或使用已有的测试项目引用
  1. 配置 Web 项目启动层
  • .NET5

在需要测试的 Web 项目启动层添加 FakeStarup.cs

FakeStarup.cs
namespace WebApplication1;

/// <summary>
/// 供集成测试使用
/// </summary>
public class FakeStartup
{
}
  • .NET6+

.NET6+)编辑 WebApplication1.csproj,添加 <InternalsVisibleTo Include="集成测试项目名称" />

<ItemGroup>
<InternalsVisibleTo Include="TestProject2" />
</ItemGroup>

.NET6+)编辑 Program.cs 添加以下代码:

public partial class Program { }
  1. 编写测试 Web 项目接口测试案例
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace TestProject2;

public class UnitTest1 : IClassFixture<WebApplicationFactory<WebApplication1.FakeStartup>>
{
private readonly WebApplicationFactory<WebApplication1.FakeStartup> _factory;
public UnitTest1(WebApplicationFactory<WebApplication1.FakeStartup> factory)
{
_factory = factory;
}

[Theory]
[InlineData("/default")]
public async Task TestEnsureSuccessStatusCode(string url)
{
using var client = _factory.CreateClient();
using var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
}
}
主机更多配置说明

如需添加更多配置,如环境变量设置,可通过以下方式:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;

namespace TestProject2;

public class UnitTest1 : IClassFixture<WebApplicationFactory<WebApplication1.FakeStartup>>
{
private readonly WebApplicationFactory<WebApplication1.FakeStartup> _factory;
public UnitTest1(WebApplicationFactory<WebApplication1.FakeStartup> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Stage"); // 设置环境
});
}

// ....
}
.NET6+ 泛型参数

如果是 .NET6+(含),只需要将 WebApplicationFactory<WebApplication1.FakeStartup> 泛型参数改为:WebApplicationFactory<Program> 即可。

/default 接口对应控制器定义如下:

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers
{
[ApiController]
[Route("[controller]")]
public class DefaultController : ControllerBase
{
[HttpGet]
public string Get()
{
return "Furion 集成测试";
}
}
}
  1. 允许测试

36.1.9.2 独立主机方式测试

独立主机的方式就是利用单元测试的每一个测试案例构建主机进行测试。

  1. 创建 Xunit 单元测试项目
  1. 添加 Microsoft.AspNetCore.Mvc.Testing 微软提供的集成测试拓展
  1. 各种创建主机方式示例
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit;

namespace TestProject3;

public class UnitTest1
{
/// <summary>
/// 创建主机并注册服务
/// </summary>
/// <remarks>可用来判断服务是否注册</remarks>
[Fact]
public void Test1()
{
var builder = WebApplication.CreateBuilder();

// 注册服务
builder.Services.AddScoped<IYourService, YourService>();

using var app = builder.Build();
var services = app.Services;

services.GetRequiredService<IYourService>();
}

/// <summary>
/// 测试配置
/// </summary>
/// <remarks>比如添加 JSON 文件配置后读取</remarks>
[Fact]
public void Test2()
{
var builder = WebApplication.CreateBuilder();
var host = builder.Host;
host.ConfigureAppConfiguration(builder =>
{
builder.Sources.Clear();
});

var config = builder.Configuration["配置"];

// 判断不为空
}
}

36.1.9.3 系统集成/环境/配置部署测试

有时候我们需要测试 Web 主机各种情况,比如端口是否有效,环境配置是否有效,系统集成情况等等,这时候只需要添加 Microsoft.AspNetCore.TestHost 拓展,然后在测试类顶部贴:

[assembly: HostingStartup(typeof(WebApplicationTests.TestHostingStartup))]

微软已经提供了非常详细的例子,这里直接放链接 https://github.com/dotnet/aspnetcore/tree/main/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests

WebApplicationTests.TestHostingStartup 为您要测试的 Web 项目启动类。

36.1.9.4 集成 Furion.Xunit 拓展

Web 集成测试支持完整的 Furion 特性,参考上面单元测试集成 Furion 章节。

Furion 纯净版

如果使用的是 Furion.Pure 则安装 Furion.Pure.Xunit 这个拓展包。

36.1.10 Assert 断言

Assert 是单元测试判定成功的依据,通常第一个参数为 期望值,第二个参数为 实际值,对比这两个值是否一致即可判断成功与否。详细的 Assert 静态方法可查阅官方库 Assert 方法

36.1.11 单元测试覆盖率

Visual Studio 提供了分析单元测试覆盖率工具,如:

36.1.12 反馈与建议

与我们交流

给 Furion 提 Issue


了解更多

想了解更多 单元测试 知识可查阅 在 .NET 中测试 章节。