.NET Core单元测试完整指南
单元测试是构建可靠、可维护软件最重要的步骤之一。在本文中,我们将通过一个简单的博客应用程序真实示例,逐步介绍如何使用xUnit、Moq和FluentAssertions在.NET Core中编写单元测试。
1. 创建测试项目
假设您有一个名为BlogApp的现有API项目。我们将创建一个测试项目来验证名为PostService的服务类的行为。
1
2
|
dotnet new xunit -n BlogApp.Tests
dotnet add BlogApp.Tests reference ../BlogApp/BlogApp.csproj
|
📁 文件夹结构:
1
2
3
4
5
6
7
8
|
BlogApp/
┣ Controllers/
┣ Services/
┣ Models/
┣ BlogApp.csproj
BlogApp.Tests/
┣ PostServiceTests.cs
┣ BlogApp.Tests.csproj
|
2. 安装依赖项
我们将使用一些流行的测试库:
1
2
|
dotnet add package Moq
dotnet add package FluentAssertions
|
它们的作用:
- Moq → 帮助创建虚假(模拟)依赖项以隔离测试
- FluentAssertions → 使测试断言可读且表达性强
3. 我们要测试的服务
这是我们的PostService类,它从存储库获取帖子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public interface IPostRepository
{
Task<Post> GetByIdAsync(int id);
Task<List<Post>> GetAllAsync();
}
public class PostService
{
private readonly IPostRepository _repo;
public PostService(IPostRepository repo)
{
_repo = repo;
}
public async Task<Post> GetPostByIdAsync(int id)
{
if (id <= 0)
throw new ArgumentException("Invalid ID");
var post = await _repo.GetByIdAsync(id);
if (post == null)
throw new KeyNotFoundException("Post not found");
return post;
}
}
|
4. 使用xUnit、Moq和FluentAssertions编写单元测试
让我们编写三个测试来覆盖有效和无效情况。
文件:BlogApp.Tests/PostServiceTests.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
using Xunit;
using Moq;
using System.Threading.Tasks;
using System.Collections.Generic;
using FluentAssertions;
public class PostServiceTests
{
[Fact]
public async Task GetPostByIdAsync_ShouldReturnPost_WhenIdIsValid()
{
// 准备
var mockRepo = new Mock<IPostRepository>();
mockRepo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(new Post { Id = 1, Title = "Test Post" });
var service = new PostService(mockRepo.Object);
// 执行
var result = await service.GetPostByIdAsync(1);
// 断言
result.Should().NotBeNull();
result.Title.Should().Be("Test Post");
}
[Fact]
public async Task GetPostByIdAsync_ShouldThrowArgumentException_WhenIdIsInvalid()
{
var mockRepo = new Mock<IPostRepository>();
var service = new PostService(mockRepo.Object);
Func<Task> act = async () => await service.GetPostByIdAsync(0);
await act.Should().ThrowAsync<ArgumentException>()
.WithMessage("Invalid ID");
}
[Fact]
public async Task GetPostByIdAsync_ShouldThrowKeyNotFoundException_WhenPostNotFound()
{
var mockRepo = new Mock<IPostRepository>();
mockRepo.Setup(r => r.GetByIdAsync(2))
.ReturnsAsync((Post)null);
var service = new PostService(mockRepo.Object);
Func<Task> act = async () => await service.GetPostByIdAsync(2);
await act.Should().ThrowAsync<KeyNotFoundException>()
.WithMessage("Post not found");
}
}
|
5. 运行测试
要执行所有测试,只需运行:
预期输出:
1
2
|
Starting test execution...
Passed! - 3 passed, 0 failed
|
单元测试最佳实践
- 保持测试隔离 - 永远不要依赖真实数据库或API
- 使用描述性测试名称,如
GetPostByIdAsync_ShouldReturnPost_WhenIdIsValid
- 使测试具有确定性 - 它们应该一致地通过或失败
- 在CI/CD流水线中集成测试(例如GitHub Actions、Azure DevOps)
- 使用FluentAssertions编写表达性强、可读性好的测试代码
额外提示 - 何时使用集成测试
单元测试在隔离环境中验证小段逻辑。如果需要验证端到端功能(如API + 数据库 + 存储库),请改为编写集成测试。
总结
单元测试不仅仅是关于代码覆盖率 - 它是关于信心。使用xUnit、Moq和FluentAssertions,您可以轻松编写清晰、可维护的测试,使您的.NET Core应用程序更可靠且更易于维护。
推荐资源
从GitHub获取示例代码