【AI Generate】TestContainers从零开始分步教程
使用Claude Code分析了开源框架netcorepal-cloud-framework的集成测试部分,并出具了搭建教程。方便日后开发时搭建
TestContainers初学者详细教程 (SQL Server版本)
从零开始学习容器化测试,包含详细的概念解释和实践指导
📚 目录
- 基础概念讲解
- 环境准备和项目初始化
- 创建基础项目结构
- 配置依赖包管理
- 实现基础设施层
- 创建单容器测试固件
- 实现Repository层集成测试
- 添加Web API集成测试
- 实现多容器编排测试
- 性能优化和最佳实践
- CI/CD集成配置
1. 基础概念讲解
1.1 什么是TestContainers?
TestContainers 是一个测试库,它让我们可以在测试中使用真实的外部依赖(如数据库、缓存、消息队列等),而不是模拟(Mock)它们。
🤔 为什么需要TestContainers?
传统测试方式的问题:
// ❌ 传统方式:使用Mock模拟数据库
[Test]
public void Test_SaveProduct()
{
// 这只是假的数据库,不能测试真实的SQL语句、事务等
var mockRepository = new Mock<IProductRepository>();
mockRepository.Setup(r => r.Add(It.IsAny<Product>())).Returns(new Product { Id = 1 });
}
TestContainers方式的优势:
// ✅ TestContainers方式:使用真实的SQL Server数据库
[Test]
public void Test_SaveProduct()
{
// 这是真实的SQL Server数据库,可以测试真实的SQL语句、事务、索引等
using var sqlContainer = new MsSqlBuilder().Build();
await sqlContainer.StartAsync();
// ... 使用真实数据库进行测试
}
📊 对比表格
| 特性 | Mock测试 | TestContainers |
|---|---|---|
| 真实性 | 模拟行为,可能与实际不符 | 使用真实数据库,100%真实 |
| SQL测试 | 无法测试复杂SQL | 可以测试所有SQL特性 |
| 事务测试 | 无法测试事务行为 | 可以测试完整事务逻辑 |
| 性能测试 | 无法测试真实性能 | 可以进行性能基准测试 |
| 维护成本 | 需要维护Mock逻辑 | 自动与生产环境保持一致 |
1.2 什么是容器(Container)?
容器 就像一个"轻量级的虚拟机",它包含了运行应用程序所需的一切:代码、运行时、系统工具、系统库等。
🏠 生活中的类比
想象容器就像集装箱:
- 标准化:无论装什么货物,集装箱的尺寸都是标准的
- 隔离性:不同集装箱里的货物不会相互影响
- 可移植:可以在船上、卡车上、火车上使用
- 一致性:在任何地方打开集装箱,里面的货物都是一样的
💻 技术层面的理解
# 这就是启动一个SQL Server容器
docker run -d \
--name my-sqlserver \
-e ACCEPT_EULA=Y \
-e SA_PASSWORD=MyPassword123! \
-p 1433:1433 \
mcr.microsoft.com/mssql/server:2022-latest
# 解释每个参数:
# -d: 在后台运行(detached)
# --name: 给容器起个名字
# -e: 设置环境变量(SQL Server需要这些配置)
# -p: 端口映射(容器内1433端口映射到主机1433端口)
1.3 什么是多容器编排?
多容器编排 是指同时运行和管理多个容器,让它们协同工作。
🎭 剧院演出的类比
想象一场音乐剧演出:
- 数据库容器 = 乐团:提供数据存储服务
- 缓存容器 = 灯光师:提供快速访问服务
- 消息队列容器 = 音响师:处理异步消息
- 编排 = 导演:协调所有角色同时开始、同时结束
💡 为什么需要多容器编排?
现代应用通常需要多个服务:
单独启动的问题:
// ❌ 问题:需要手动管理每个容器的生命周期
var sqlContainer = new MsSqlBuilder().Build();
var redisContainer = new RedisBuilder().Build();
// 如果SQL Server启动失败了,Redis还在运行,造成资源浪费
await sqlContainer.StartAsync(); // 可能失败
await redisContainer.StartAsync(); // 还是会执行
编排的优势:
// ✅ 解决方案:统一管理所有容器
public class MultiContainerFixture
{
public async Task InitializeAsync()
{
// 同时启动所有容器,任何一个失败都会停止所有容器
await Task.WhenAll(
_sqlContainer.StartAsync(),
_redisContainer.StartAsync(),
_rabbitContainer.StartAsync()
);
}
}
1.4 什么是集成测试 vs 单元测试?
🏗️ 建筑施工的类比
单元测试 = 测试每一块砖头的质量
[Test]
public void Test_Product_Creation()
{
// 只测试Product类本身的逻辑
var product = new Product("iPhone", 999.99m);
Assert.Equal("iPhone", product.Name);
Assert.Equal(999.99m, product.Price);
}
集成测试 = 测试整面墙的稳固性
[Test]
public void Test_Product_Save_To_Database()
{
// 测试Product类 + Repository类 + SQL Server数据库的整体协作
var product = new Product("iPhone", 999.99m);
await _repository.SaveAsync(product);
var saved = await _repository.GetByIdAsync(product.Id);
Assert.NotNull(saved);
}
📊 对比表格
| 测试类型 | 测试范围 | 运行速度 | 发现问题类型 | 何时使用 |
|---|---|---|---|---|
| 单元测试 | 单个类/方法 | 很快(毫秒级) | 逻辑错误 | 开发过程中频繁运行 |
| 集成测试 | 多个组件协作 | 较慢(秒级) | 组件间协作问题 | 提交代码前运行 |
1.5 什么是Repository模式?
Repository模式 是一种设计模式,它把数据访问逻辑封装起来,让业务代码不需要知道数据是如何存储的。
🏪 商店购物的类比
想象你去商店买东西:
- 你(业务代码) 只需要说:"我要买一瓶可乐"
- 店员(Repository) 知道可乐放在哪个货架,如何找到它
- 仓库(数据库) 是实际存放商品的地方
你不需要知道:
- 可乐放在第几排第几列
- 仓库的布局如何
- 进货和库存管理的细节
💻 代码层面的理解
// ❌ 没有Repository模式:业务代码直接操作数据库
public class OrderService
{
public async Task CreateOrder(Order order)
{
// 业务代码需要知道数据库的细节
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var command = new SqlCommand("INSERT INTO Orders...", connection);
// 大量的数据库操作代码...
}
}
// ✅ 使用Repository模式:业务代码只关心业务逻辑
public class OrderService
{
private readonly IOrderRepository _orderRepository;
public async Task CreateOrder(Order order)
{
// 业务代码只需要调用简单的方法
await _orderRepository.SaveAsync(order);
}
}
2. 环境准备和项目初始化
2.1 为什么需要这些工具?
🛠️ 工具箱类比
就像木匠需要不同的工具一样,我们也需要准备开发工具:
| 工具 | 作用 | 类比 |
|---|---|---|
| .NET 8.0 SDK | 编译和运行C#代码 | 木匠的基本工具箱 |
| Docker Desktop | 运行容器 | 电动工具(自动化) |
| Visual Studio | 代码编辑器 | 工作台 |
| Git | 版本控制 | 图纸存档系统 |
🔍 详细安装说明
2.1.1 安装.NET 8.0 SDK
# 验证安装
dotnet --version
# 应该看到类似:8.0.100 的输出
# 如果没有安装,请访问:
# https://dotnet.microsoft.com/download
📝 知识点:什么是SDK?
- SDK = Software Development Kit(软件开发工具包)
- 包含了编译器、运行时、调试器等开发需要的所有工具
- 就像买了一套完整的乐高积木,而不是只买几个零件
2.1.2 安装Docker Desktop
# 验证Docker是否正常运行
docker --version
docker ps
# 如果看到类似输出说明安装成功:
# Docker version 24.0.0
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
📝 知识点:为什么需要Docker?
- 传统方式:在你的电脑上直接安装SQL Server(占用空间,难以卸载)
- 容器方式:SQL Server运行在隔离的容器中(用完就删除,不留痕迹)
2.2 创建解决方案结构
🏗️ 建筑蓝图类比
创建项目结构就像绘制建筑蓝图,需要先规划好各个房间的用途。
# 创建解决方案目录(这是你的"地基")
mkdir TestContainersDemo
cd TestContainersDemo
# 创建解决方案文件(这是你的"总蓝图")
dotnet new sln -n TestContainersDemo
📝 知识点:什么是解决方案(Solution)?
- 解决方案 是一个容器,可以包含多个项目
- 就像一个小区包含多栋楼一样
.sln文件记录了这个小区里有哪些楼(项目)
🏠 项目结构设计原理
我们要创建的项目结构:
TestContainersDemo/ # 小区
├── src/ # 住宅区
│ ├── TestContainersDemo.Api/ # 门面房(接待客户)
│ ├── TestContainersDemo.Core/ # 核心区(重要业务)
│ └── TestContainersDemo.Infrastructure/ # 基础设施(水电煤)
└── test/ # 质检区
├── TestContainersDemo.UnitTests/ # 单元质检
└── TestContainersDemo.IntegrationTests/ # 整体质检
📝 知识点:为什么要这样分层?
这叫做 "Clean Architecture"(干净架构):
-
Core(核心层):
- 包含业务逻辑和实体
- 不依赖任何外部技术
- 就像房子的承重墙,最重要且最稳定
-
Infrastructure(基础设施层):
- 处理数据库、文件系统等外部资源
- 就像房子的水电管道,为核心提供支持
-
Api(接口层):
- 处理用户请求
- 就像房子的门窗,是外界与内部沟通的通道
优点:
- 职责清晰:每层只做自己该做的事
- 易于测试:可以单独测试每一层
- 易于维护:修改一层不会影响其他层
3. 创建基础项目结构
3.1 逐步创建项目
📝 详细命令解释
# 创建Web API项目(门面)
dotnet new webapi -n TestContainersDemo.Api -o src/TestContainersDemo.Api
# 命令解释:
# dotnet new: 创建新项目的命令
# webapi: 项目模板(已经包含了Web API的基本结构)
# -n: name,项目名称
# -o: output,输出目录
📝 知识点:什么是Web API?
- API = Application Programming Interface(应用程序接口)
- 就像餐厅的菜单,告诉客户可以点什么菜
- Web API 通过HTTP协议提供服务
例子:
客户请求:GET /api/products/1
服务器响应:{ "id": 1, "name": "iPhone", "price": 999.99 }
# 创建核心业务项目(大脑)
dotnet new classlib -n TestContainersDemo.Core -o src/TestContainersDemo.Core
# 知识点:classlib = 类库,包含可重用的代码
# 创建基础设施项目(手脚)
dotnet new classlib -n TestContainersDemo.Infrastructure -o src/TestContainersDemo.Infrastructure
# 这里会放数据库访问、文件操作等代码
# 创建测试项目
dotnet new xunit -n TestContainersDemo.UnitTests -o test/TestContainersDemo.UnitTests
dotnet new xunit -n TestContainersDemo.IntegrationTests -o test/TestContainersDemo.IntegrationTests
# 知识点:xunit 是 .NET 中流行的测试框架
3.2 设置项目依赖关系
🔗 依赖关系的重要性
依赖关系 就像食物链,规定了谁可以使用谁:
📝 原则:依赖方向应该向内
- ✅ 外层可以依赖内层(Api可以使用Core)
- ❌ 内层不能依赖外层(Core不能使用Api)
🔨 设置依赖的命令
# API项目需要使用Core和Infrastructure
dotnet add src/TestContainersDemo.Api reference src/TestContainersDemo.Core
dotnet add src/TestContainersDemo.Api reference src/TestContainersDemo.Infrastructure
# 为什么?
# - API需要调用业务逻辑(Core)
# - API需要进行数据库操作(Infrastructure)
📝 知识点:为什么这样设置依赖?
想象一个餐厅:
- Api层 = 服务员:接待客户,传递订单
- Core层 = 厨师:处理业务逻辑(做菜)
- Infrastructure层 = 采购员:获取原材料(数据)
服务员需要知道厨师在哪(依赖Core),也需要知道原材料在哪(依赖Infrastructure),但厨师不需要知道服务员的具体工作方式。
4. 配置依赖包管理
4.1 什么是NuGet包?
📦 包裹快递类比
NuGet包 就像网购的包裹:
- 有人已经写好了有用的代码
- 打包发布到NuGet仓库
- 你可以"下单"安装到自己的项目中
例子:
# 就像在网上买了一个"数据库连接工具包"
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
4.2 为什么需要集中化包管理?
🏪 连锁店管理类比
想象你有多家连锁店:
❌ 没有统一管理的问题:
- 北京店:使用A品牌的收银系统v1.0
- 上海店:使用A品牌的收银系统v2.0
- 广州店:使用B品牌的收银系统v1.5
结果: 版本混乱,难以维护,可能出现兼容性问题
✅ 集中化管理的好处:
- 所有店都使用统一版本的系统
- 升级时一次性升级所有店
- 避免版本冲突
💻 代码层面的实现
创建 Directory.Packages.props 文件:
<Project>
<PropertyGroup>
<!-- 启用集中化包管理 -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- 在这里定义所有包的版本 -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<!-- ... 更多包 -->
</ItemGroup>
</Project>
📝 知识点:这样做的好处
- 版本一致性:所有项目使用相同版本
- 易于升级:只需要改一个地方
- 减少冲突:避免版本不兼容问题
4.3 SQL Server相关的包
🔧 我们需要的工具包
<!-- Entity Framework Core SQL Server包 -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<!-- 为什么需要? -->
<!-- 这个包让我们的C#代码可以与SQL Server数据库通信 -->
📝 知识点:什么是Entity Framework Core (EF Core)?
EF Core 是一个 ORM(Object-Relational Mapping,对象关系映射)工具。
生活类比:
- 你 = C#程序员(只会说中文)
- 数据库 = 外国人(只会说SQL语言)
- EF Core = 翻译官
没有EF Core时:
// 你需要写复杂的SQL语句
var command = new SqlCommand(
"INSERT INTO Products (Name, Price) VALUES (@name, @price)",
connection);
command.Parameters.Add("@name", product.Name);
command.Parameters.Add("@price", product.Price);
使用EF Core后:
// 你只需要用简单的C#代码
await _context.Products.AddAsync(product);
await _context.SaveChangesAsync();
🧪 TestContainers相关的包
<!-- TestContainers核心包 -->
<PackageVersion Include="Testcontainers" Version="3.10.0" />
<!-- SQL Server容器包 -->
<PackageVersion Include="Testcontainers.MsSql" Version="3.10.0" />
<!-- Redis容器包(用于缓存测试) -->
<PackageVersion Include="Testcontainers.Redis" Version="3.10.0" />
📝 知识点:为什么需要这些包?
-
Testcontainers - 核心功能:
- 启动和停止容器
- 管理容器生命周期
- 处理端口映射
-
Testcontainers.MsSql - SQL Server专用功能:
- 预配置好的SQL Server容器
- 自动处理SQL Server特有的启动参数
- 提供连接字符串生成
-
Testcontainers.Redis - Redis专用功能:
- 用于测试缓存功能
- 后面我们会用到
5. 实现基础设施层
5.1 什么是实体(Entity)?
🏗️ 建筑图纸类比
实体 就像建筑图纸,描述了"东西"的结构和属性。
在我们的电商系统中:
- Product(产品) = 商品的基本信息
- Order(订单) = 客户购买记录
- OrderItem(订单项) = 订单中的具体商品
💻 Product实体详解
namespace TestContainersDemo.Core.Entities;
public class Product
{
public int Id { get; set; } // 商品编号(主键)
public string Name { get; set; } = string.Empty; // 商品名称
public string Description { get; set; } = string.Empty; // 商品描述
public decimal Price { get; set; } // 价格
public int StockQuantity { get; set; } // 库存数量
public DateTime CreatedAt { get; set; } // 创建时间
public DateTime UpdatedAt { get; set; } // 最后更新时间
}
📝 知识点:为什么要设计这些属性?
-
Id(主键):
- 每个商品的唯一标识
- 就像身份证号码,永远不重复
-
Name和Description:
- 用
string.Empty而不是null - 避免空引用异常(NullReferenceException)
- 用
-
Price使用decimal:
- 而不是double或float
- 因为decimal精度更高,适合金钱计算
- 避免浮点数计算误差(如0.1 + 0.2 ≠ 0.3)
-
CreatedAt和UpdatedAt:
- 审计字段,记录数据的生命周期
- 方便调试和数据分析
5.2 什么是DbContext?
🏢 公司办公室类比
DbContext 就像一个公司的办公室:
- 办公室 = DbContext
- 各个部门 = DbSet(表)
- 员工 = 实体对象
- 公司制度 = 数据库配置
public class ApplicationDbContext : DbContext
{
// 这些就是"部门"
public DbSet<Product> Products => Set<Product>(); // 产品部门
public DbSet<Order> Orders => Set<Order>(); // 订单部门
public DbSet<OrderItem> OrderItems => Set<OrderItem>(); // 订单详情部门
}
🔧 SQL Server特定配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置Product表
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id); // 设置主键
entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
// SQL Server特定:设置decimal精度
entity.Property(e => e.Price).HasColumnType("decimal(18,2)");
// SQL Server特定:设置datetime2类型(更高精度)
entity.Property(e => e.CreatedAt).HasColumnType("datetime2");
// 创建索引(加快查询速度)
entity.HasIndex(e => e.Name);
});
}
📝 知识点:为什么需要这些配置?
-
HasColumnType("decimal(18,2)"):
- 告诉SQL Server:这个字段总共18位,小数点后2位
- 如:999999999999999.99(16位整数 + 2位小数)
-
HasColumnType("datetime2"):
- SQL Server的新日期类型,精度更高
- 支持微秒级精度
-
HasIndex(e => e.Name):
- 在Name字段上创建索引
- 就像书的目录,加快查找速度
5.3 什么是Repository模式详解
🏪 图书馆管理员类比
想象你去图书馆借书:
没有Repository的情况:
// 你需要自己去书架找书
public class StudentService
{
public Book FindBook(string title)
{
// 你需要知道书在哪个楼层、哪个区域、哪个书架
// 你需要知道图书馆的分类系统
// 你需要处理借书卡、登记等细节
// 非常复杂!
}
}
有Repository的情况:
// 管理员帮你找书
public class StudentService
{
private readonly IBookRepository _bookRepository;
public async Task<Book> FindBook(string title)
{
// 你只需要告诉管理员书名
return await _bookRepository.GetByTitleAsync(title);
}
}
💻 Repository接口设计
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id); // 根据ID查找
Task<List<Product>> GetAllAsync(); // 获取所有产品
Task<Product> AddAsync(Product product); // 添加产品
Task UpdateAsync(Product product); // 更新产品
Task DeleteAsync(int id); // 删除产品
Task<bool> ExistsAsync(int id); // 检查产品是否存在
}
📝 知识点:为什么用接口?
- 抽象化:业务代码不需要知道数据如何存储
- 可测试性:可以创建假的Repository进行测试
- 可替换性:可以换成不同的数据库而不影响业务代码
生活例子:
- 接口 = 充电线接口(USB-C)
- 实现 = 具体的充电器
无论是华为、小米、还是苹果的充电器,只要符合USB-C接口标准,都可以给你的手机充电。
6. 创建单容器测试固件
6.1 什么是测试固件(Test Fixture)?
🎬 电影拍摄类比
测试固件 就像电影拍摄的布景:
- 拍摄前:搭建布景、准备道具、调试灯光
- 拍摄时:演员在布景中表演(测试在固件中运行)
- 拍摄后:拆除布景、清理现场
代码类比:
public class DatabaseFixture
{
// 搭建布景
public async Task InitializeAsync()
{
await _sqlContainer.StartAsync(); // 启动数据库容器
await CreateDatabase(); // 创建数据库表
}
// 拆除布景
public async Task DisposeAsync()
{
await _sqlContainer.StopAsync(); // 停止容器
}
}
6.2 SQL Server容器配置详解
🐳 容器启动参数解释
_msSqlContainer = new MsSqlBuilder()
// 使用微软官方SQL Server 2022镜像
.WithImage("mcr.microsoft.com/mssql/server:2022-CU18-ubuntu-22.04")
// 设置SA用户密码(SA = System Administrator)
.WithPassword("StrongPass123!")
// 接受SQL Server许可协议
.WithEnvironment("ACCEPT_EULA", "Y")
// 使用Express版本(免费版,适合开发测试)
.WithEnvironment("MSSQL_PID", "Express")
// 测试完成后自动清理容器
.WithCleanUp(true)
.Build();
📝 知识点:每个参数的作用
-
WithImage:
- 指定Docker镜像
- 就像指定要下载哪个版本的软件安装包
-
WithPassword:
- SQL Server需要管理员密码
- 密码必须符合复杂性要求(大小写+数字+特殊字符)
-
ACCEPT_EULA:
- EULA = End User License Agreement(最终用户许可协议)
- 必须设置为"Y"表示同意微软的使用条款
-
MSSQL_PID:
- PID = Product ID(产品标识)
- Express = 免费版本,足够测试使用
-
WithCleanUp:
- 自动清理,避免测试后留下垃圾容器
6.3 容器健康检查的重要性
🏥 体检类比
问题场景:
// ❌ 危险的做法:不检查容器是否真正就绪
await _sqlContainer.StartAsync();
// 这时候容器可能还在启动中...
var context = CreateDbContext(); // 可能失败!
就像病人刚进医院,医生还没检查就开始手术一样危险!
💊 健康检查的实现
private async Task WaitForSqlServerReady()
{
var maxAttempts = 30; // 最多尝试30次
var delay = TimeSpan.FromSeconds(2); // 每次等待2秒
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
// 尝试连接数据库
await using var context = CreateDbContext();
await context.Database.CanConnectAsync();
Console.WriteLine($"SQL Server ready! (attempt {attempt})");
return; // 连接成功,退出循环
}
catch (Exception ex)
{
Console.WriteLine($"SQL Server not ready (attempt {attempt}/{maxAttempts}): {ex.Message}");
if (attempt == maxAttempts)
{
throw new InvalidOperationException("SQL Server failed to start!");
}
await Task.Delay(delay); // 等待2秒再试
}
}
}
📝 知识点:为什么需要重试循环?
SQL Server启动过程:
- 容器启动 (1-2秒)
- SQL Server进程启动 (5-10秒)
- 数据库引擎初始化 (10-20秒)
- 准备接受连接 (总共可能需要30秒)
如果不等待就尝试连接,就会失败!
6.4 数据清理策略
🧹 清洁工作类比
为什么需要清理数据?
想象你是实验室的研究员:
- 实验1:测试化学反应A
- 实验2:测试化学反应B
如果实验1的残留物质影响了实验2,那么实验2的结果就不准确了!
💻 SQL Server特定的清理方法
public async Task CleanDatabaseAsync()
{
await using var context = CreateDbContext();
// 步骤1:删除所有数据
await context.Database.ExecuteSqlRawAsync("DELETE FROM OrderItems");
await context.Database.ExecuteSqlRawAsync("DELETE FROM Orders");
await context.Database.ExecuteSqlRawAsync("DELETE FROM Products");
// 步骤2:重置自增ID(Identity种子)
await context.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT ('Products', RESEED, 0)");
await context.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT ('Orders', RESEED, 0)");
await context.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT ('OrderItems', RESEED, 0)");
}
📝 知识点:为什么要重置Identity种子?
没有重置的情况:
测试1创建产品 → ID=1, 2, 3
清理数据,但ID计数器还在3
测试2创建产品 → ID=4, 5, 6 (不是从1开始!)
重置后的情况:
测试1创建产品 → ID=1, 2, 3
清理数据并重置计数器
测试2创建产品 → ID=1, 2, 3 (从1开始,更可预测)
7. 实现Repository层集成测试
7.1 什么是集成测试?
🚗 汽车测试类比
单元测试 = 测试单个零件
[Test]
public void Test_Engine_Starts()
{
var engine = new Engine();
Assert.True(engine.Start()); // 只测试引擎能否启动
}
集成测试 = 测试整车性能
[Test]
public void Test_Car_Can_Drive()
{
var car = new Car(engine, wheels, transmission, brakes);
car.Start();
car.Drive();
Assert.Equal(60, car.Speed); // 测试所有零件协作的结果
}
7.2 测试基类的作用
🏫 学校班级管理类比
没有基类的问题:
// 每个测试类都要重复相同的代码
public class MathTestClass
{
private DatabaseFixture _fixture;
public async Task Setup() { /* 重复的设置代码 */ }
public async Task Cleanup() { /* 重复的清理代码 */ }
}
public class EnglishTestClass
{
private DatabaseFixture _fixture; // 重复!
public async Task Setup() { /* 重复的设置代码 */ } // 重复!
public async Task Cleanup() { /* 重复的清理代码 */ } // 重复!
}
使用基类的好处:
public abstract class BaseIntegrationTest // 就像"班级管理规则"
{
protected readonly SqlServerDatabaseFixture DatabaseFixture;
// 统一的设置和清理逻辑
public virtual async Task DisposeAsync()
{
await DatabaseFixture.CleanDatabaseAsync();
}
}
public class MathTestClass : BaseIntegrationTest // 数学课遵循班级规则
{
// 只需要写数学相关的测试,不用重复基础代码
}
7.3 AutoFixture的作用
🏭 工厂流水线类比
手工创建测试数据的问题:
[Test]
public void Test_Product_Creation()
{
// 每次都要手工编写这些数据,很累!
var product1 = new Product
{
Name = "iPhone 15",
Price = 999.99m,
Description = "Latest iPhone",
StockQuantity = 10
};
var product2 = new Product
{
Name = "Samsung Galaxy",
Price = 899.99m,
Description = "Latest Samsung phone",
StockQuantity = 5
};
// ... 需要更多数据时更痛苦
}
使用AutoFixture自动生成:
[Test]
public void Test_Product_Creation()
{
// 工厂自动生成测试数据
var products = _fixture.Build<Product>()
.Without(p => p.Id) // 不设置ID(数据库自动生成)
.Without(p => p.CreatedAt) // 不设置创建时间(代码自动设置)
.CreateMany(100); // 一次生成100个产品!
// 专注于测试逻辑,而不是数据准备
}
📝 知识点:AutoFixture的智能之处
-
自动生成合理的值:
- 字符串:会生成"Name123456"这样的值
- 数字:会生成随机但合理的数字
- 日期:会生成合理的日期值
-
避免重复:每次生成的数据都不一样
-
可控制:可以指定哪些字段不要自动生成
7.4 测试用例设计原则
🎯 射箭训练类比
好的测试就像射箭训练,需要:
- 明确目标:知道要测试什么
- 准备环境:设置靶子和弓箭
- 执行动作:射箭
- 验证结果:看是否命中目标
💻 AAA模式详解
[Fact]
public async Task AddAsync_ShouldCreateProduct_WhenValidProductProvided()
{
// Arrange(准备)- 设置测试环境和数据
await using var context = DatabaseFixture.CreateDbContext();
var repository = new ProductRepository(context);
var product = _fixture.Build<Product>()
.Without(p => p.Id)
.Without(p => p.CreatedAt)
.Without(p => p.UpdatedAt)
.Create();
// Act(执行)- 执行要测试的动作
var result = await repository.AddAsync(product);
// Assert(验证)- 检查结果是否符合预期
Assert.True(result.Id > 0); // ID应该大于0
Assert.NotEqual(DateTime.MinValue, result.CreatedAt); // 创建时间应该被设置
// 进一步验证:数据真的保存到数据库了吗?
var savedProduct = await repository.GetByIdAsync(result.Id);
Assert.NotNull(savedProduct);
Assert.Equal(product.Name, savedProduct.Name);
}
📝 知识点:为什么要验证两次?
- 第一次验证:检查方法的返回值是否正确
- 第二次验证:检查数据是否真的保存到数据库
这确保了我们不是在测试一个"假"的保存操作。
7.5 Theory测试的强大功能
🧪 化学实验类比
普通测试 = 做一次实验
[Fact]
public void Test_Water_Boils_At_100_Degrees()
{
var result = Water.Heat(100);
Assert.Equal("Steam", result.State);
}
Theory测试 = 做多组对比实验
[Theory]
[InlineData(true)] // 产品存在的情况
[InlineData(false)] // 产品不存在的情况
public async Task ExistsAsync_ShouldReturnCorrectResult_BasedOnProductExistence(bool shouldExist)
{
// 一个测试方法,测试两种情况
// 这样可以确保逻辑在各种情况下都正确
}
好处:
- 减少重复代码:一个测试方法覆盖多种场景
- 提高覆盖率:确保边界条件都被测试
- 容易维护:修改测试逻辑只需要改一个地方
8. 添加Web API集成测试
8.1 什么是Web API集成测试?
🏪 餐厅服务测试类比
Repository测试 = 测试厨房(后厨能否做菜)
Web API测试 = 测试整个餐厅服务流程
从客户进门到吃完饭的完整流程:
- 客户点菜(HTTP请求)
- 服务员记录(Controller接收)
- 厨房做菜(Repository操作)
- 上菜(HTTP响应)
[Fact]
public async Task Create_Product_Should_Work_End_To_End()
{
// 模拟客户行为:通过HTTP调用API
var request = new CreateProductRequest("iPhone", "Latest phone", 999.99m, 10);
var response = await _client.PostAsJsonAsync("/api/products", request);
// 检查服务是否成功
response.EnsureSuccessStatusCode();
// 检查返回的产品信息是否正确
var product = await response.Content.ReadFromJsonAsync<Product>();
Assert.NotNull(product);
Assert.Equal("iPhone", product.Name);
}
8.2 WebApplicationFactory的作用
🎭 戏剧彩排类比
WebApplicationFactory 就像戏剧彩排的舞台:
真实演出舞台(生产环境):
- 有真实的观众
- 使用正式的道具
- 连接真实的数据库
彩排舞台(测试环境):
- 没有观众(不对外提供服务)
- 使用临时道具(TestContainers数据库)
- 可以随时重置场景
public class WebApplicationTestFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// 替换生产环境的数据库配置
builder.ConfigureServices(services =>
{
// 移除生产数据库配置
var descriptor = services.SingleOrDefault(d =>
d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
services.Remove(descriptor);
// 使用测试容器数据库
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(ConnectionString));
});
// 使用测试环境配置
builder.UseEnvironment("Testing");
}
}
📝 知识点:为什么要替换数据库配置?
- 隔离性:测试不能影响生产数据
- 可控性:可以创建特定的测试场景
- 可重复性:每次测试都是干净的环境
8.3 HTTP状态码的含义
🚦 交通信号灯类比
HTTP状态码就像交通信号灯,告诉你发生了什么:
| 状态码 | 含义 | 交通灯类比 | 示例场景 |
|---|---|---|---|
| 200 OK | 成功 | 🟢 绿灯 - 通行 | 成功获取产品信息 |
| 201 Created | 创建成功 | 🟢 绿灯 - 新路开通 | 成功创建新产品 |
| 404 Not Found | 找不到 | 🔴 红灯 - 此路不通 | 产品不存在 |
| 400 Bad Request | 请求错误 | 🟡 黄灯 - 注意 | 提交的数据格式错误 |
[Fact]
public async Task GetById_ShouldReturn404_WhenProductNotExists()
{
// Act - 请求不存在的产品
var response = await _client.GetAsync("/api/products/999");
// Assert - 应该返回404状态码
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
8.4 JSON序列化和反序列化
📦 包装和拆包类比
序列化 = 打包邮寄
// 把C#对象"打包"成JSON字符串,通过网络发送
var product = new Product { Name = "iPhone", Price = 999.99m };
var json = JsonSerializer.Serialize(product);
// 结果:{"Name":"iPhone","Price":999.99}
反序列化 = 拆包取货
// 把收到的JSON字符串"拆包"成C#对象
var json = """{"Name":"iPhone","Price":999.99}""";
var product = JsonSerializer.Deserialize<Product>(json);
// 结果:Product对象,Name="iPhone", Price=999.99m
在测试中的应用:
[Fact]
public async Task Create_ShouldReturnCreatedProduct_WhenValidDataProvided()
{
// 序列化:把请求对象转换为JSON发送
var request = new CreateProductRequest("iPhone", "Description", 999.99m, 10);
var response = await _client.PostAsJsonAsync("/api/products", request);
// 反序列化:把响应JSON转换为C#对象
var product = await response.Content.ReadFromJsonAsync<Product>();
Assert.NotNull(product);
}
9. 实现多容器编排测试
9.1 什么是多容器编排?深度解析
🎼 交响乐团类比
想象一场交响音乐会:
单容器测试 = 钢琴独奏
- 只有一台钢琴(SQL Server容器)
- 简单直接,但功能有限
多容器编排 = 完整交响乐团
- 钢琴(SQL Server)- 提供主要的数据存储
- 小提琴(Redis缓存)- 提供快速的数据访问
- 大鼓(RabbitMQ消息队列)- 处理异步通信
- 指挥(编排系统)- 协调所有乐器同步演奏
🏗️ 现代应用架构
现代应用很少只使用一个数据库:
为什么需要多个服务?
-
SQL Server:
- 存储重要的业务数据
- 支持复杂查询和事务
- 但是相对较慢
-
Redis缓存:
- 存储频繁访问的数据
- 访问速度极快(内存存储)
- 但是数据可能丢失
-
消息队列:
- 处理异步任务
- 解耦不同的服务
- 提高系统可靠性
9.2 缓存的作用和原理
🏪 便利店类比
没有缓存的情况:
客户:我要买可乐
店员:好的,我去仓库帮您找(走到后面仓库,翻箱倒柜)
店员:找到了!(返回前台)
时间:5分钟
有缓存的情况:
客户:我要买可乐
店员:好的!(直接从收银台旁边的小冰箱拿出可乐)
时间:10秒
💻 代码层面的缓存
public class ProductService
{
private readonly IProductRepository _repository;
private readonly ICacheService _cache;
public async Task<Product> GetProductAsync(int id)
{
// 1. 先检查缓存
var cacheKey = $"product:{id}";
var cachedProduct = await _cache.GetAsync<Product>(cacheKey);
if (cachedProduct != null)
{
Console.WriteLine("从缓存获取数据 - 很快!");
return cachedProduct; // 从缓存返回,超快!
}
// 2. 缓存没有,查询数据库
Console.WriteLine("从数据库获取数据 - 较慢");
var product = await _repository.GetByIdAsync(id);
// 3. 存入缓存,下次就快了
if (product != null)
{
await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(15));
}
return product;
}
}
📝 知识点:缓存的优缺点
优点:
- 访问速度极快(内存 vs 磁盘,差距几千倍)
- 减少数据库压力
- 提升用户体验
缺点:
- 数据可能不是最新的
- 需要处理缓存失效的情况
- 占用内存空间
9.3 容器编排的挑战
🚦 交通协调类比
想象你要协调多个路口的红绿灯:
挑战1:启动顺序
❌ 错误的启动顺序:
1. 先启动应用程序
2. 数据库还没准备好 → 应用程序崩溃!
✅ 正确的启动顺序:
1. 先启动所有基础设施(数据库、缓存)
2. 等待它们完全就绪
3. 最后启动应用程序
挑战2:健康检查
// 不能只检查容器是否启动了
if (container.State == ContainerState.Running) // 这还不够!
// 还要检查服务是否真正可用
await database.CanConnectAsync(); // 数据库能连接吗?
await redis.PingAsync(); // Redis能响应吗?
挑战3:资源清理
public async Task DisposeAsync()
{
// 必须正确清理所有容器,避免资源泄露
await Task.WhenAll(
_sqlContainer.DisposeAsync().AsTask(),
_redisContainer.DisposeAsync().AsTask(),
_rabbitMqContainer.DisposeAsync().AsTask()
);
}
9.4 多容器测试的实际场景
🛒 电商购物车场景
让我们测试一个真实的业务场景:用户添加商品到购物车
[Fact]
public async Task AddToCart_ShouldWork_WithDatabaseAndCache()
{
// Arrange - 准备数据
var product = await CreateProductInDatabase(); // 在SQL Server中创建商品
// Act - 用户操作:添加到购物车
var cartItem = new CartItem { ProductId = product.Id, Quantity = 2 };
// 1. 数据库操作:记录购物车项(持久化存储)
await _cartRepository.AddItemAsync(cartItem);
// 2. 缓存操作:快速访问(临时存储)
var cacheKey = $"cart:user123";
await _cacheService.SetAsync(cacheKey, cartItem, TimeSpan.FromMinutes(30));
// Assert - 验证两个系统都工作正常
// 验证数据库:数据被持久化保存
var savedCartItem = await _cartRepository.GetItemAsync(cartItem.Id);
Assert.NotNull(savedCartItem);
Assert.Equal(2, savedCartItem.Quantity);
// 验证缓存:数据可以快速访问
var cachedCartItem = await _cacheService.GetAsync<CartItem>(cacheKey);
Assert.NotNull(cachedCartItem);
Assert.Equal(2, cachedCartItem.Quantity);
}
这个测试验证了:
- 数据一致性:数据库和缓存中的数据是否一致
- 性能协作:数据库负责持久化,缓存负责快速访问
- 故障处理:如果缓存失败,数据库数据是否还在
9.5 容器间通信测试
📞 电话会议类比
在多容器环境中,不同的服务需要相互通信:
[Fact]
public async Task DatabaseTransaction_ShouldNot_AffectCache()
{
// 这个测试验证一个重要概念:事务边界
// Arrange
var product = CreateTestProduct();
// Act - 在事务中操作数据库,同时操作缓存
try
{
using var transaction = await _dbContext.Database.BeginTransactionAsync();
// 1. 数据库操作(在事务中)
await _productRepository.AddAsync(product);
// 2. 缓存操作(不在事务中)
await _cacheService.SetAsync($"product:{product.Id}", product);
// 3. 回滚事务(模拟业务失败场景)
await transaction.RollbackAsync();
}
catch
{
// 忽略异常,这是预期的
}
// Assert - 验证事务边界的正确性
// 数据库中应该没有数据(事务被回滚了)
var dbProduct = await _productRepository.GetByIdAsync(product.Id);
Assert.Null(dbProduct);
// 缓存中应该有数据(缓存不受数据库事务影响)
var cachedProduct = await _cacheService.GetAsync<Product>($"product:{product.Id}");
Assert.NotNull(cachedProduct);
}
📝 知识点:这个测试的重要性
- 理解事务边界:数据库事务不能控制缓存操作
- 数据一致性问题:如果处理不当,可能导致数据不一致
- 实际业务场景:帮助开发者理解分布式系统的复杂性
10. 性能优化和最佳实践
10.1 为什么性能优化很重要?
⏰ 时间就是金钱类比
想象你经营一家工厂:
慢速度的测试:
每个测试需要2分钟
100个测试 = 200分钟 = 3.3小时
开发者每天运行3次 = 10小时!
优化后的测试:
每个测试需要10秒
100个测试 = 1000秒 = 16.7分钟
开发者每天运行3次 = 50分钟
结果: 每天节省9小时!开发者更愿意频繁运行测试。
10.2 SQL Server容器优化技巧
🏎️ 赛车调校类比
就像赛车需要调校才能跑得更快,SQL Server容器也需要优化:
public static async Task ConfigureForTestingAsync(ApplicationDbContext context)
{
// 1. 设置恢复模式为简单模式(减少日志开销)
await context.Database.ExecuteSqlRawAsync(@"
ALTER DATABASE [{0}] SET RECOVERY SIMPLE;
", context.Database.GetDbConnection().Database);
// 2. 设置自动收缩(测试环境,节省空间)
await context.Database.ExecuteSqlRawAsync(@"
ALTER DATABASE [{0}] SET AUTO_SHRINK ON;
");
// 3. 预热连接池(第一次连接通常最慢)
await context.Database.ExecuteSqlRawAsync("SELECT 1");
}
📝 知识点:每个优化的作用
-
RECOVERY SIMPLE:
- 简化事务日志管理
- 测试环境不需要复杂的恢复策略
- 类比:考试时用铅笔而不是钢笔,可以擦除修改
-
AUTO_SHRINK ON:
- 自动释放未使用的空间
- 测试环境数据量变化大
- 类比:自动整理房间,不要的东西自动清理
-
预热连接:
- 第一次连接最慢(需要建立连接池)
- 提前执行简单查询建立连接
- 类比:汽车启动后先热车,后续行驶更顺畅
10.3 容器复用策略
♻️ 回收利用类比
传统方式:每次用完就扔
[Fact]
public async Task Test1()
{
var container = new MsSqlBuilder().Build();
await container.StartAsync(); // 启动需要30秒
// 进行测试...
await container.StopAsync(); // 用完就停止,浪费!
}
[Fact]
public async Task Test2()
{
var container = new MsSqlBuilder().Build();
await container.StartAsync(); // 又要启动30秒!
// 进行测试...
await container.StopAsync();
}
优化方式:回收利用
// 使用xUnit的ICollectionFixture,在多个测试间共享容器
[CollectionDefinition("Database Collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
}
[Collection("Database Collection")] // 这些测试共享同一个容器
public class ProductRepositoryTests { }
[Collection("Database Collection")] // 复用同一个容器
public class OrderRepositoryTests { }
效果:
- 容器只启动一次(30秒)
- 多个测试复用(节省时间)
- 但是要注意数据隔离(每个测试后清理数据)
10.4 并行测试配置
🏭 工厂生产线类比
串行测试 = 单条生产线
测试1 → 测试2 → 测试3 → 测试4
总时间 = 所有测试时间之和
并行测试 = 多条生产线
生产线1:测试1 → 测试3
生产线2:测试2 → 测试4
总时间 = 最慢那条线的时间
⚙️ xUnit配置
在 xunit.runner.json 中:
{
"parallelizeAssembly": true, // 启用并行测试
"parallelizeTestCollections": false, // 但同一Collection内不并行
"maxParallelThreads": 4, // 最多4个线程
"longRunningTestSeconds": 60 // 超过60秒算长时间运行
}
📝 知识点:为什么有些设置为false?
-
parallelizeTestCollections: false:
- 同一Collection共享容器
- 如果并行运行会互相干扰
- 类比:同一个厨房不能同时做两道菜
-
maxParallelThreads: 4:
- 不是越多越好
- 太多线程会争抢资源(CPU、内存、磁盘IO)
- 类比:厨房只有一个灶台,派10个厨师也没用
10.5 性能监控和基准测试
📊 体检报告类比
定期检查测试性能,就像定期体检:
public abstract class PerformanceTestBase : BaseIntegrationTest
{
private readonly Stopwatch _stopwatch = new();
public override Task InitializeAsync()
{
_stopwatch.Start(); // 开始计时
return base.InitializeAsync();
}
public override async Task DisposeAsync()
{
_stopwatch.Stop(); // 停止计时
var testName = GetType().Name;
var elapsedMs = _stopwatch.ElapsedMilliseconds;
// 记录执行时间
Console.WriteLine($"[{testName}] 执行时间: {elapsedMs}ms");
// 警告慢测试
if (elapsedMs > 5000) // 超过5秒
{
Console.WriteLine($"[警告] {testName} 运行过慢: {elapsedMs}ms");
}
await base.DisposeAsync();
}
}
基准测试示例:
[Fact]
public async Task BulkInsert_ShouldComplete_WithinTimeLimit()
{
// Arrange
var products = CreateTestProducts(1000); // 创建1000个测试产品
var stopwatch = Stopwatch.StartNew();
// Act
foreach (var product in products)
{
await _repository.AddAsync(product);
}
stopwatch.Stop();
// Assert - 性能要求
Console.WriteLine($"插入1000个产品耗时: {stopwatch.ElapsedMilliseconds}ms");
Assert.True(stopwatch.ElapsedMilliseconds < 30000,
$"批量插入耗时过长: {stopwatch.ElapsedMilliseconds}ms");
// Assert - 数据正确性
var savedCount = await _repository.CountAsync();
Assert.Equal(1000, savedCount);
}
📝 知识点:性能测试的价值
- 发现性能退化:新代码是否让测试变慢了?
- 建立基准线:知道正常的性能水平
- 优化指导:哪些部分需要优化?
11. CI/CD集成配置
11.1 什么是CI/CD?
🏭 自动化工厂类比
传统软件发布 = 手工作坊
开发者写代码 → 手工测试 → 手工打包 → 手工部署
问题:容易出错,效率低下,不一致
CI/CD = 自动化工厂
开发者提交代码 → 自动测试 → 自动打包 → 自动部署
好处:减少人为错误,提高效率,保证一致性
🔄 CI/CD流程详解
CI (Continuous Integration) - 持续集成:
CD (Continuous Deployment) - 持续部署:
11.2 GitHub Actions配置详解
🤖 机器人助手类比
GitHub Actions就像雇了一个机器人助手,每当你提交代码时:
name: SQL Server Integration Tests # 机器人的名字
on: # 什么时候工作?
push:
branches: [ main, develop ] # 当推送到主分支时
pull_request:
branches: [ main, develop ] # 当创建拉取请求时
jobs: # 具体要做什么工作?
integration-tests: # 工作名称
runs-on: ubuntu-latest # 在Ubuntu系统上运行
每个步骤的作用:
steps:
- name: Checkout code # 1. 获取代码
uses: actions/checkout@v4 # 就像从仓库取出图纸
- name: Setup .NET # 2. 准备工具
uses: actions/setup-dotnet@v3 # 安装.NET编译器
with:
dotnet-version: '8.0.x'
- name: Build solution # 3. 编译项目
run: dotnet build # 把源代码编译成程序
- name: Run integration tests # 4. 运行测试
run: dotnet test # 验证程序是否正确
env:
DOCKER_HOST: unix:///var/run/docker.sock # 启用Docker支持
11.3 为什么需要Docker支持?
🏠 租房类比
在自己电脑上测试:
- 就像在自己家里做饭
- 你知道厨具在哪里,调料怎么放
- 但是客人来你家不一定适应
在CI环境中测试:
- 就像在朋友家做饭
- 环境完全不同,可能没有你需要的工具
- 需要Docker容器提供"便携厨房"
# CI环境通常没有安装SQL Server
# 但有Docker,可以运行SQL Server容器
docker run mcr.microsoft.com/mssql/server:2022-latest
11.4 本地开发脚本
🛠️ 瑞士军刀类比
一个好的测试脚本就像瑞士军刀,集成了多种功能:
# PowerShell脚本 (Windows)
Write-Host "开始SQL Server容器测试..." -ForegroundColor Yellow
# 1. 健康检查 - 检查Docker是否运行
$dockerInfo = docker info 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Host "错误:Docker未运行,请启动Docker" -ForegroundColor Red
exit 1
}
# 2. 环境准备 - 拉取必要的镜像
Write-Host "准备SQL Server镜像..." -ForegroundColor Yellow
docker pull mcr.microsoft.com/mssql/server:2022-CU18-ubuntu-22.04
# 3. 清理 - 避免之前测试的影响
Write-Host "清理之前的容器..." -ForegroundColor Yellow
docker container prune -f
# 4. 构建 - 编译代码
Write-Host "构建解决方案..." -ForegroundColor Yellow
dotnet build --configuration Release
# 5. 测试 - 运行测试套件
Write-Host "运行集成测试..." -ForegroundColor Yellow
dotnet test test/TestContainersDemo.IntegrationTests `
--no-build `
--configuration Release `
--verbosity normal
# 6. 结果报告
if ($LASTEXITCODE -eq 0) {
Write-Host "所有测试通过!" -ForegroundColor Green
} else {
Write-Host "测试失败!" -ForegroundColor Red
# 显示容器日志帮助调试
docker logs $(docker ps -q --filter "ancestor=mcr.microsoft.com/mssql/server")
exit 1
}
📝 知识点:脚本的价值
- 一致性:每次运行都执行相同的步骤
- 自动化:减少手工操作的错误
- 可重复:新团队成员可以轻松运行测试
- 调试友好:出错时提供有用的信息
11.5 测试结果报告
📋 考试成绩单类比
就像考试后要看成绩单,测试后也要看报告:
- name: Upload test results # 上传测试结果
uses: actions/upload-artifact@v3
if: always() # 无论成功失败都上传
with:
name: test-results
path: TestResults/
测试报告包含:
- 通过/失败数量:多少个测试成功了?
- 执行时间:哪些测试运行缓慢?
- 错误详情:失败的测试具体什么问题?
- 代码覆盖率:多少代码被测试覆盖了?
使用测试配置文件:
<!-- test.runsettings -->
<RunSettings>
<RunConfiguration>
<MaxCpuCount>4</MaxCpuCount> <!-- 最多用4个CPU核心 -->
<TestSessionTimeout>600000</TestSessionTimeout> <!-- 10分钟超时 -->
</RunConfiguration>
<TestRunParameters>
<Parameter name="SqlServerContainerTimeout" value="120" /> <!-- 自定义参数 -->
</TestRunParameters>
</RunSettings>
11.6 本地开发环境 vs CI环境
🏠 在家 vs 🏢 在公司类比
| 特性 | 本地环境(在家) | CI环境(在公司) |
|---|---|---|
| 运行速度 | 较快(熟悉环境) | 较慢(需要准备环境) |
| 调试能力 | 很强(可以断点调试) | 有限(只能看日志) |
| 环境一致性 | 因人而异 | 完全一致 |
| 资源限制 | 取决于个人电脑 | 有固定的资源配额 |
| 网络环境 | 取决于个人网络 | 稳定的数据中心网络 |
最佳实践:
- 本地环境:用于快速开发和调试
- CI环境:用于最终验证和质量保证
- 两者结合:确保代码在各种环境下都能工作
🎯 总结和下一步
你已经学会了什么?
🎓 知识清单
基础概念:
- ✅ 容器技术:理解了什么是Docker容器,为什么比传统虚拟机更轻量
- ✅ TestContainers:学会了用真实数据库替代Mock进行测试
- ✅ 多容器编排:理解了现代应用需要多个服务协作的复杂性
架构设计:
- ✅ Clean Architecture:学会了分层设计的原则和好处
- ✅ Repository模式:理解了如何抽象数据访问层
- ✅ 依赖注入:学会了如何管理对象之间的依赖关系
测试策略:
- ✅ 单元测试 vs 集成测试:理解了不同测试的适用场景
- ✅ 测试固件:学会了如何准备和清理测试环境
- ✅ AAA模式:掌握了测试用例的标准结构
性能优化:
- ✅ 容器复用:学会了如何提高测试执行速度
- ✅ 并行测试:理解了如何平衡速度和资源使用
- ✅ 性能监控:学会了如何发现和解决性能问题
DevOps实践:
- ✅ CI/CD流程:理解了自动化的重要性和实现方式
- ✅ 环境一致性:学会了如何保证开发、测试、生产环境的一致
🚀 下一步建议
🏗️ 继续深入学习
技术深化:
-
微服务测试:
- 学习如何测试服务间的通信
- 掌握契约测试(Contract Testing)
- 理解分布式系统的复杂性
-
性能测试进阶:
- 负载测试:模拟大量用户同时访问
- 压力测试:测试系统的极限
- 混沌工程:故意制造故障测试系统韧性
-
安全测试:
- SQL注入测试
- 身份认证和授权测试
- 敏感数据保护测试
工具扩展:
-
更多数据库:
- PostgreSQL容器测试
- MongoDB文档数据库测试
- Elasticsearch搜索引擎测试
-
消息队列测试:
- RabbitMQ消息测试
- Apache Kafka流处理测试
- Redis发布订阅测试
-
监控和日志:
- 集成Prometheus监控
- 使用ELK Stack进行日志分析
- 分布式链路追踪
📚 推荐学习资源
书籍推荐:
- 《测试驱动开发》- Kent Beck
- 《重构:改善既有代码的设计》- Martin Fowler
- 《领域驱动设计》- Eric Evans
- 《微服务架构设计模式》- Chris Richardson
在线资源:
- Microsoft文档:https://docs.microsoft.com/aspnet/core
- TestContainers官网:https://www.testcontainers.org/
- Docker官方教程:https://docs.docker.com/get-started/
- xUnit文档:https://xunit.net/docs/getting-started/netcore/cmdline
🎊 恭喜你!
通过这个详细的教程,你已经从TestContainers的初学者成长为能够:
- 🛠️ 独立搭建完整的容器化测试环境
- 🧪 编写高质量的集成测试用例
- ⚡ 优化测试性能,提高开发效率
- 🚀 集成CI/CD,实现自动化测试
- 🎯 理解现代软件开发的最佳实践
这些技能将在你的职业生涯中非常宝贵,不仅适用于.NET项目,也适用于其他技术栈的项目。
记住: 好的测试不仅是为了发现错误,更是为了给你信心去重构、优化和扩展你的代码。有了这套完整的测试基础设施,你可以更加自信地面对复杂的业务需求和技术挑战!
继续练习,持续改进,你一定会成为一名优秀的软件工程师!🌟

浙公网安备 33010602011771号