掌握 .NET 8 中最小 API 的单元和集成测试:高质量代码的最佳实践
在 .NET 8 中开发最小 API 时,测试是确保 API 可靠、可扩展且可维护的关键步骤。结构良好的单元和集成测试可以显著提高 API 的质量,帮助您及早发现错误,并保证您的代码在各种场景中都能按预期运行。
在这篇博文中,我们将介绍如何在 .NET 8 中为最小 API 实现单元和集成测试,并介绍帮助您交付高质量 API 的最佳实践。
为什么测试最少的 API 是必不可少的
最小的 API 虽然轻量级且易于实现,但需要彻底测试,就像任何其他 API 一样。单元测试验证单个组件或方法的行为,而集成测试确保整个系统(包括数据库或其他 API 等外部依赖项)正常工作。
测试您的 Minimal API 有几个好处:
- 在进行更新或重构时防止回归。
- 通过确保每个组件按预期工作来提高可维护性。
- 通过在开发过程的早期捕获问题来减少 bug。
- 确保不同环境和用例的可靠性。
1. 为最小 API 设置单元测试
单元测试涉及测试应用程序中的最小部分(通常是单个方法或函数),这些部分与其依赖项隔离开来。
步骤 1:创建单元测试项目
首先,在解决方案中创建一个单元测试项目。如果您使用的是 xUnit(最流行的 .NET 测试框架之一),则可以使用以下命令创建测试项目:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>dotnet new xunit <span style="color:var(--syntax-error-color)">-n</span> MinimalApiTests
</code></span></span>
在测试项目中添加对 Minimal API 项目的引用:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>dotnet add reference ../MinimalApiProject/MinimalApiProject.csproj
</code></span></span>
第 2 步:模拟依赖项
由于单元测试应该独立运行,因此您需要模拟依赖项,例如数据库访问或外部服务。对于模拟,您可以使用 Moq,一个流行的模拟库:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>dotnet add package Moq
</code></span></span>
下面是一个如何模拟服务并测试依赖于它的方法的示例:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">interface</span> <span style="color:var(--syntax-name-color)">IWeatherService</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">Task</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-declaration-color)">string</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-name-color)">GetWeatherAsync</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">class</span> <span style="color:var(--syntax-name-color)">WeatherApi</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">readonly</span> <span style="color:var(--syntax-text-color)">IWeatherService</span> <span style="color:var(--syntax-text-color)">_weatherService</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-name-color)">WeatherApi</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">IWeatherService</span> <span style="color:var(--syntax-text-color)">weatherService</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">_weatherService</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-text-color)">weatherService</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">async</span> <span style="color:var(--syntax-text-color)">Task</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-declaration-color)">string</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-name-color)">GetWeather</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-declaration-color)">await</span> <span style="color:var(--syntax-text-color)">_weatherService</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">GetWeatherAsync</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
对于单元测试:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">class</span> <span style="color:var(--syntax-name-color)">WeatherApiTests</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-text-color)">Fact</span><span style="color:var(--syntax-text-color)">]</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">async</span> <span style="color:var(--syntax-text-color)">Task</span> <span style="color:var(--syntax-name-color)">GetWeather_ReturnsExpectedWeather</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-comment-color)">// Arrange</span>
<span style="color:var(--syntax-declaration-color)">var</span> <span style="color:var(--syntax-text-color)">mockWeatherService</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-text-color)">Mock</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">IWeatherService</span><span style="color:var(--syntax-text-color)">>();</span>
<span style="color:var(--syntax-text-color)">mockWeatherService</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">Setup</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">service</span> <span style="color:var(--syntax-text-color)">=></span> <span style="color:var(--syntax-text-color)">service</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">GetWeatherAsync</span><span style="color:var(--syntax-text-color)">()).</span><span style="color:var(--syntax-name-color)">ReturnsAsync</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"Sunny"</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-declaration-color)">var</span> <span style="color:var(--syntax-text-color)">weatherApi</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-name-color)">WeatherApi</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">mockWeatherService</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Object</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-comment-color)">// Act</span>
<span style="color:var(--syntax-declaration-color)">var</span> <span style="color:var(--syntax-text-color)">result</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">await</span> <span style="color:var(--syntax-text-color)">weatherApi</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">GetWeather</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-comment-color)">// Assert</span>
<span style="color:var(--syntax-text-color)">Assert</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">Equal</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"Sunny"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">result</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
此测试模拟并确保该方法返回预期结果,而无需实际调用真实的 weather 服务。IWeatherServiceWeatherApi.GetWeather()
2. 为最少的 API 编写集成测试
与单元测试不同,集成测试可确保系统的不同部分协同工作,包括外部依赖项,例如数据库、第三方服务或文件系统。集成测试的范围通常更广,对于测试 Minimal API 的真实行为至关重要。
第 1 步:使用 WebApplicationFactory 进行集成测试
.NET 使使用 WebApplicationFactory 为 Minimal API 编写集成测试变得容易。此工厂设置一个测试服务器来模拟实际的 API 环境,允许您针对 API 运行请求并验证响应。
安装必要的测试包:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.InMemory
</code></span></span>
然后,创建集成测试:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">class</span> <span style="color:var(--syntax-name-color)">WeatherApiIntegrationTests</span> <span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">IClassFixture</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">WebApplicationFactory</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Program</span><span style="color:var(--syntax-text-color)">>></span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">readonly</span> <span style="color:var(--syntax-text-color)">HttpClient</span> <span style="color:var(--syntax-text-color)">_client</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-name-color)">WeatherApiIntegrationTests</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">WebApplicationFactory</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Program</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-text-color)">factory</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">_client</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-text-color)">factory</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">CreateClient</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-text-color)">Fact</span><span style="color:var(--syntax-text-color)">]</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">async</span> <span style="color:var(--syntax-text-color)">Task</span> <span style="color:var(--syntax-name-color)">GetWeather_ReturnsExpectedResult</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-comment-color)">// Act</span>
<span style="color:var(--syntax-declaration-color)">var</span> <span style="color:var(--syntax-text-color)">response</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">await</span> <span style="color:var(--syntax-text-color)">_client</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">GetAsync</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"/weather"</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">response</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">EnsureSuccessStatusCode</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-declaration-color)">var</span> <span style="color:var(--syntax-text-color)">result</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">await</span> <span style="color:var(--syntax-text-color)">response</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Content</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">ReadAsStringAsync</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-comment-color)">// Assert</span>
<span style="color:var(--syntax-text-color)">Assert</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">Equal</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"Sunny"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">result</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
在此示例中,WebApplicationFactory 会启动 API 的测试实例,您可以使用 .HttpClient
第 2 步:在集成测试中模拟外部依赖项
对于集成测试,模拟数据库等外部依赖项非常重要,以确保您的测试一致运行,而无需真正的数据库连接。您可以使用 InMemory 数据库或模拟服务。
配置 InMemory 数据库的示例:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">class</span> <span style="color:var(--syntax-name-color)">StartupTest</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">void</span> <span style="color:var(--syntax-name-color)">ConfigureServices</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">IServiceCollection</span> <span style="color:var(--syntax-text-color)">services</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">services</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">AddDbContext</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">AppDbContext</span><span style="color:var(--syntax-text-color)">>(</span><span style="color:var(--syntax-text-color)">options</span> <span style="color:var(--syntax-text-color)">=></span>
<span style="color:var(--syntax-text-color)">options</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">UseInMemoryDatabase</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"TestDb"</span><span style="color:var(--syntax-text-color)">));</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
这允许您在隔离环境中测试 API 与数据库的交互。
3. 单元和集成测试的最佳实践
为确保您的测试有效且可维护,请遵循以下最佳实践:
1. 为 Happy 和 Unhappy paths 编写测试
- 不仅要测试预期的场景 (满意路径),还要测试边缘情况、错误处理和无效输入 (不满意路径)。
2. 保持单元测试的隔离
- 通过使用模拟对象和依赖项注入,确保单元测试与外部系统隔离。
3. 在 CI/CD 中自动执行测试
- 使用持续集成 (CI) 工具(如 GitHub Actions、Azure Pipelines 或 Jenkins)在每次提交时自动运行测试。
4. 确保快速反馈
- 保持单元测试的轻量级和快速性。集成测试可能需要更长的时间,但通过专注于核心交互来确保它们高效运行。
5. 关注可读性和可维护性
- 编写清晰、描述性的测试名称,并保持测试小而有重点。这提高了可读性,并帮助未来的开发人员了解每个测试的目的。
6. 使用内存数据库进行集成测试
- 为避免在集成测试中对数据库进行复杂的设置,请使用 SQLite 或 Entity Framework InMemory 等内存中数据库来模拟真实的数据库行为。
7. 确定测试覆盖率的优先级
- 以高测试覆盖率为目标,但也要确保您的测试有意义。专注于关键业务逻辑和 API 终端节点。
4. 运行测试并分析结果
使用 dotnet test 命令在 .NET 中运行测试非常简单:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>dotnet <span style="color:var(--syntax-text-color)">test</span>
</code></span></span>
此命令将运行您的所有单元和集成测试,并在控制台中显示结果。对于详细报告,您可以与提供测试结果图形视图的测试报告工具或 CI/CD 平台集成。
结论
测试是构建高质量 API 的一个重要方面。通过遵循本指南中概述的单元和集成测试策略,您可以确保 .NET 8 中的最小 API 可靠、可维护并准备好用于生产。从模拟单元测试中的依赖项到使用 WebApplicationFactory 进行集成测试,每一层测试在 API 的稳定性中都起着至关重要的作用。
确保在您的项目中采用这些最佳实践,您将看到代码质量和 API 部署的信心得到提高。