概述
silky 框架通过 Silky.TestBase 模块为开发者提供了集成测试基础设施。与纯单元测试不同,集成测试会启动真实的 IoC 容器(Autofac),加载模块依赖,使得被测代码在接近真实运行环境的上下文中执行,能够验证服务间的依赖注入、配置读取、模块初始化等行为。
安装
<PackageReference Include="Silky.TestBase" Version="3.9.1" />
<!-- 使用 xUnit 测试框架 -->
<PackageReference Include="xunit" Version="2.x.x" />
<PackageReference Include="Shouldly" Version="4.x.x" /> <!-- 可选的断言库 -->
核心类型
SilkyIntegratedTest<TStartupModule>
SilkyIntegratedTest<TStartupModule> 是所有集成测试类的基类,泛型参数 TStartupModule 是测试专用的模块入口类。
构造时,基类会:
- 创建宿主环境(默认环境名称为
Testing) - 读取测试项目下的配置文件(
appsettings.json、appsettings.Testing.json等) - 构建 Autofac 容器,完整执行模块初始化流程
- 暴露
ServiceProvider、Configuration、Engine等属性供测试使用
构造完成后,可通过 GetRequiredService<T>() 获取已注册的服务实例。
| 成员 | 说明 |
|---|---|
ServiceProvider | 根服务容器(IServiceProvider) |
TestServiceScope | 测试用服务作用域(IServiceScope) |
Configuration | 测试环境的 IConfiguration |
Engine | Silky 引擎实例(IEngine) |
GetRequiredService<T>() | 从测试作用域中获取服务 |
BeforeAddApplication(services) | 钩子:在模块初始化前向容器追加注册 |
AfterAddApplication(services) | 钩子:在模块初始化后向容器追加注册 |
CreateConfigurationBuilder() | 钩子:自定义配置构建方式 |
CreateHostEnvironment() | 钩子:自定义测试宿主环境 |
当测试类析构时,Dispose() 会自动关闭 TestServiceScope,释放所有 Scoped 服务。
基本用法
步骤一:创建测试模块
测试模块通过 [DependsOn] 特性声明对被测业务模块的依赖:
using Silky.Core.Modularity;
[DependsOn(
typeof(OrderApplicationModule), // 被测业务模块
typeof(AccountApplicationModule) // 其他依赖模块
)]
public class OrderTestModule : SilkyModule
{
// 可在此处重写 ConfigureServices、Initialize 等方法进行测试特定配置
}
步骤二:创建测试类
继承 SilkyIntegratedTest<TStartupModule>,在构造函数中获取被测服务:
using Silky.TestBase;
using Shouldly;
using Xunit;
public class OrderAppServiceTests : SilkyIntegratedTest<OrderTestModule>
{
private readonly IOrderAppService _orderAppService;
public OrderAppServiceTests()
{
_orderAppService = GetRequiredService<IOrderAppService>();
}
[Fact]
public async Task CreateOrderAsync_ShouldReturnOrderId()
{
// Arrange
var input = new CreateOrderInput
{
ProductId = 1,
Quantity = 2,
Address = "测试地址"
};
// Act
var orderId = await _orderAppService.CreateOrderAsync(input);
// Assert
orderId.ShouldBeGreaterThan(0L);
}
[Fact]
public async Task GetOrderAsync_WithInvalidId_ShouldThrowException()
{
await Should.ThrowAsync<UserFriendlyException>(
() => _orderAppService.GetAsync(-1));
}
}
步骤三:测试配置文件
在测试项目下添加 appsettings.json 或 appsettings.Testing.json,配置测试所需的数据库连接、服务地址等:
{
"ConnectionStrings": {
"Default": "Server=localhost;Database=TestDb;User Id=sa;Password=xxx;"
}
}
提示
测试环境名称默认为 Testing,框架会按照 appsettings.json → appsettings.Testing.json 的顺序加载配置,测试专用配置会覆盖基础配置。
自定义测试基础设施
替换真实依赖为 Mock
通过重写 BeforeAddApplication 钩子,可以在模块加载前注入 Mock 对象,替换不便在测试环境中运行的外部依赖:
public class OrderAppServiceTests : SilkyIntegratedTest<OrderTestModule>
{
protected override void BeforeAddApplication(IServiceCollection services)
{
// 用 Mock 替换外部支付服务
var mockPaymentService = new Mock<IPaymentService>();
mockPaymentService
.Setup(s => s.ChargeAsync(It.IsAny<ChargeInput>()))
.ReturnsAsync(new ChargeResult { Success = true });
services.AddTransient(_ => mockPaymentService.Object);
}
}
自定义宿主环境
重写 CreateHostEnvironment() 以自定义环境名称或 ContentRoot:
using Moq;
using Microsoft.Extensions.Hosting;
protected override IHostEnvironment CreateHostEnvironment()
{
var mockEnv = new Mock<IHostEnvironment>();
mockEnv.Setup(e => e.EnvironmentName).Returns("IntegrationTest");
mockEnv.Setup(e => e.ApplicationName).Returns(GetType().Name);
mockEnv.Setup(m => m.ContentRootPath).Returns(Directory.GetCurrentDirectory());
return mockEnv.Object;
}
使用内存数据库
结合 EFCore 的 UseInMemoryDatabase,在内存中运行数据库操作:
[DependsOn(typeof(OrderApplicationModule))]
public class OrderTestModule : SilkyModule
{
public override void ConfigureServices(IServiceCollection services)
{
// 将真实数据库连接替换为内存数据库
services.Configure<DbContextOptions<OrderDbContext>>(options =>
{
options.UseInMemoryDatabase("TestOrderDb");
});
}
}
常见问题
问:集成测试是否会启动 RPC 服务?
不会。Silky.TestBase 只启动 IoC 容器,不会启动 Tcp 监听、注册中心连接等网络服务。如果被测服务包含需要 RPC 调用的路径,需要使用 Mock 替换相关依赖。
问:如何在测试中模拟已登录用户?
NullSession.Instance 的属性从 RpcContext 中读取,没有直接的 setter。在测试中,通过向 RpcContext 注入 Claim 附件来模拟已登录用户:
using System.Security.Claims;
using Silky.Core.Runtime.Rpc;
[Fact]
public async Task GetCurrentUserOrders_ShouldReturnUserOrders()
{
// 模拟当前登录用户(通过 RpcContext 注入 Claim)
RpcContext.Context.SetInvokeAttachment(ClaimTypes.NameIdentifier, 12345L);
RpcContext.Context.SetInvokeAttachment(ClaimTypes.Name, "testuser");
var orders = await _orderAppService.GetCurrentUserOrdersAsync();
orders.ShouldNotBeEmpty();
}
问:集成测试的速度是否很慢?
首次构造测试类时需要初始化 IoC 容器,耗时通常在几百毫秒到几秒之间,取决于模块数量和初始化逻辑。可以利用 xUnit 的 IClassFixture<T> 在同一测试类的多个测试方法间共享容器实例,避免重复初始化:
public class OrderTestFixture : SilkyIntegratedTest<OrderTestModule> { }
public class OrderQueryTests : IClassFixture<OrderTestFixture>
{
private readonly IOrderAppService _orderAppService;
public OrderQueryTests(OrderTestFixture fixture)
{
_orderAppService = fixture.GetRequiredService<IOrderAppService>();
}
[Fact]
public async Task GetOrderListAsync_ShouldReturnPagedResult() { ... }
[Fact]
public async Task GetOrderAsync_WithValidId_ShouldReturnOrder() { ... }
}
