.NET Core单元测试完整指南:使用xUnit、Moq和FluentAssertions

本文详细介绍了如何在.NET Core中使用xUnit测试框架、Moq模拟库和FluentAssertions断言库编写单元测试,包含完整的项目创建、依赖安装、测试用例编写和最佳实践指南。

.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
dotnet test

预期输出:

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获取示例代码

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计