如何测试ASP.NET Core Minimal APIs(附代码示例)

345 阅读11分钟

您如何测试您的ASP.NET Core Minimal API的行为是否符合预期?你需要部署你的应用程序吗?你能用xUnitNUnitMSTest等框架编写测试吗?

在这篇文章中,你将学习测试ASP.NET Core Minimal APIs的基础知识。你将从测试一个 "hello world "端点开始,然后测试一个返回JSON数据的更复杂的API。最后,你将定制ASP.NET Core服务集合,这样你就可以为你的单元测试和集成测试定制服务。

在本篇文章结束时,你将充分了解如何确保你的ASP.NET Core Minimal API的行为符合预期,并且可以部署到生产中,即使是在星期五也是如此

前提条件

你可以在GitHub上找到本教程的源代码。如果你遇到任何问题,可以把它作为参考。

创建一个测试项目

为了开始,你需要创建一个有两个项目的解决方案:一个是包含应用程序的ASP.NET Core Minimal API,另一个是包含测试的单元测试项目。在这篇博文中,你将使用xUnit作为测试框架。

你可以在你喜欢的.NET IDE中创建这个解决方案,或者使用.NET CLI。在命令行或终端窗口,导航到你想创建项目的文件夹,并运行以下命令:

dotnet new web -o MyMinimalApi
dotnet new xunit -o MyMinimalApi.Tests
dotnet add MyMinimalApi.Tests reference MyMinimalApi
dotnet new sln
dotnet sln add MyMinimalApi
dotnet sln add MyMinimalApi.Tests

现在你有一个MyMinimalApi.sln文件和两个项目*(MyMinimalApi.csproj*用于ASP.NET Core Minimal API,MyMinimalApi.Tests.csproj用于单元测试)以及一些模板代码。测试项目也有一个对Minimal API项目的项目引用。

要运行Minimal API应用程序,你可以使用.NET CLI并指定要运行的项目:

dotnet run --project MyMinimalApi

可以使用以下.NET CLI命令来运行测试:

dotnet test

这些项目中还没有很多有用的代码。Minimal API项目包含一个Program.cs文件,其中有一个返回字符串 "Hello World!"的端点:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

测试项目*(MyMinimalApi.Tests.csproj*)包含一个模板单元测试文件UnitTest1.cs,你将在本文的后面替换它。

更新测试项目

在开始测试你的Minimal API之前,你需要对测试项目做一些更新。单元测试需要能够使用ASP.NET Core框架,所以你必须以某种方式将其引入。最简单的方法是通过添加对Microsoft.AspNetCore.Mvc.Testing 包的引用来实现。这个包还带有几个辅助类,在以后编写单元测试时是非常宝贵的。

使用你喜欢的IDE添加这个包,或者使用.NET CLI:

dotnet add MyMinimalApi.Tests package Microsoft.AspNetCore.Mvc.Testing

MyMinimalApi.Tests.csproj文件现在看起来像这样:

<Project Sdk="Microsoft.NET.Sdk">

 <PropertyGroup>
   <TargetFramework>net6.0</TargetFramework>
   <ImplicitUsings>enable</ImplicitUsings>
   <Nullable>enable</Nullable>

   <IsPackable>false</IsPackable>
 </PropertyGroup>

 <ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0" />
   <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
   <PackageReference Include="xunit" Version="2.4.1" />
   <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
     <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     <PrivateAssets>all</PrivateAssets>
   </PackageReference>
   <PackageReference Include="coverlet.collector" Version="3.1.2">
     <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     <PrivateAssets>all</PrivateAssets>
   </PackageReference>
 </ItemGroup>

 <ItemGroup>
   <ProjectReference Include="..\MinimalAPI\MinimalAPI.csproj" />
 </ItemGroup>

</Project>

你现在可以开始为你的Minimal API编写单元测试了。

"Hello World "和ASP.NET Core测试服务器

在Minimal API项目中,Program.cs已经定义了一个 "Hello World!"端点。你将首先测试这个端点。在这之前,你需要在Program.cs的底部添加以下公共部分类定义:

public partial class Program { }

之所以需要这个局部类定义,是因为默认情况下,Program.cs文件被编译成一个私有类Program ,不能被其他项目访问。通过添加这个公共部分类,测试项目将获得对Program ,并让你针对它编写测试。

MyMinimalApi.Tests项目中,将UnitTest1.cs文件重命名为HelloWorldTests.cs并更新代码:

namespace MyMinimalApi.Tests;

using Microsoft.AspNetCore.Mvc.Testing;

public class HelloWorldTests
{
    [Fact]
    public async Task TestRootEndpoint()
    {
      
    }
}

TestRootEndpoint() 测试将必须做几件事:

  • 启动ASP.NET Core Minimal API
  • 创建一个HTTP客户端,以连接到该应用程序
  • / 端点发送一个HTTP请求
  • 验证响应

在这篇文章的前面,你已经添加了一个对Microsoft.AspNetCore.Mvc.Testing 包的引用。这个包包含WebApplicationFactory<T> ,它是测试ASP.NET Core应用程序的一个重要构件。

WebApplicationFactory<T> 类创建了一个你可以测试的内存中的应用程序。它处理你的应用程序的引导,并提供一个HttpClient ,你可以用它来进行请求。

更新TestRootEndpoint() 方法中的代码:

[Fact]
public async Task TestRootEndpoint()
{
    await using var application = new WebApplicationFactory<Program>();
    using var client = application.CreateClient();

    var response = await client.GetStringAsync("/");
  
    Assert.Equal("Hello World!", response);
}

该代码使用WebApplicationFactory<Program> 。这就是你不得不添加那个公共部分类的原因!你可以使用其他公共类。你也可以使用Minimal API项目中的其他公共类,但我个人更喜欢Program ,因为每个项目中都有它。

你可以使用.NET CLI运行这个测试,看看结果:

> dotnet test

Microsoft (R) Test Execution Command Line Tool Version 17.2.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: < 1 ms - MyMinimalApi.Tests.dll (net6.0)

你创建的测试刚刚使用WebApplicationFactory<Program> 启动了你的Minimal API应用程序,并使用了由application.CreateClient() 返回的HttpClient 。使用这个客户端,测试向/ 端点发出一个HTTP GET请求。在这个例子中,你使用了GetStringAsync("/") 方法来做到这一点。然后测试断定响应符合预期。

恭喜你,你刚刚为ASP.NET Core Minimal API创建了第一个测试。

更新Minimal API项目

让我们给事情加点料吧!在大多数API中,端点将在请求和响应中使用JSON有效载荷。一个API端点可能会根据正在进行的请求返回不同的结果。它可能在成功时返回一个200 OK 状态代码,而在请求无效时返回一个400 Bad Request 状态代码,并在响应体中提供更多细节。

在本节中,你将向Minimal API添加这样一个端点。这个端点还将使用MiniValidation包对请求进行验证。

使用你喜欢的IDE添加这个包,或者使用.NET CLI:

dotnet add MyMinimalApi package MiniValidation --prerelease

MiniValidation是一个旨在为ASP.NET Core Minimal APIs带来模型验证的库。它目前只有预发布包可用。当稳定版本登陆时,你应该考虑放弃--prerelease 版本。

安装好后,在你的Minimal API中添加一个Person 类。这个类以后将被用作请求的有效载荷:

public class Person
{
    [Required, MinLength(2)]
    public string? FirstName { get; set; }

    [Required, MinLength(2)]
    public string? LastName { get; set; }

    [Required, DataType(DataType.EmailAddress)]
    public string? Email { get; set; }
}

注意,Person 类添加了来自System.ComponentModel.DataAnnotations 命名空间的验证属性。在你的Program.cs文件的顶部添加using System.ComponentModel.DataAnnotations; ,以包括该命名空间。你之前添加的MiniValidation 包可以处理这些属性,并验证请求的格式是否正确。

Minimal API还需要能够将Person 在一个数据存储中。虽然对这个数据存储的建模不在本文的范围内,但你可以定义一个IPeopleService 接口来与数据存储进行交互,以及一个实现该接口的PeopleService 类:

public interface IPeopleService
{
    string Create(Person person);
}

public class PeopleService: IPeopleService
{
    public string Create(Person person)
        => $"{person.FirstName} {person.LastName} created.";
}

在实际项目中,PeopleService 可以使用Entity Framework Core或其他存储机制来做一些更有用的事情。

现在是时候在ASP.NET Core服务集合中注册IPeopleService ,这样你的API端点就可以利用它了。把它添加为一个范围内的服务,以确保每次有请求进来时,都会创建一个新的PeopleService 的实例:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IPeopleService, PeopleService>();

// ...

你做得很好!作为本节的最后一步,你将在你的Minimal API中实现实际的API端点。/people这个端点将监听POST ,并在请求正文中接受一个Person 对象。在端点验证传入的请求后,API要么使用IPeopleService ,将对象存储在数据库中,要么返回一个验证结果:

app.MapPost("/people", (Person person, IPeopleService peopleService) =>
    !MiniValidator.TryValidate(person, out var errors)
        ? Results.ValidationProblem(errors)
        : Results.Ok(peopleService.Create(person)));

Program.cs类顶部的using语句中添加using MiniValidation; ,这样你就可以使用MiniValidator 类。

为了确认,这里是你的Program.cs现在应该是的样子:

using System.ComponentModel.DataAnnotations;
using MiniValidation;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IPeopleService, PeopleService>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/people", (Person person, IPeopleService peopleService) =>
    !MiniValidator.TryValidate(person, out var errors)
        ? Results.ValidationProblem(errors)
        : Results.Ok(peopleService.Create(person)));

app.Run();

public partial class Program { }

public interface IPeopleService
{
    string Create(Person person);
}

public class PeopleService : IPeopleService
{
    public string Create(Person person)
        => $"{person.FirstName} {person.LastName} created.";
}

public class Person
{
    [Required, MinLength(2)]
    public string? FirstName { get; set; }

    [Required, MinLength(2)]
    public string? LastName { get; set; }

    [Required, DataType(DataType.EmailAddress)]
    public string? Email { get; set; }
}

如果你愿意,你可以运行Minimal API,从你的终端测试/people 端点。

首先,使用dotnet run --project MyMinimalApi ,启动你的Minimal API,在输出中寻找localhost的URL。

如果你的终端有curl 命令,请运行:

curl -X POST --location "https://localhost:7230/people" \
    -H "Content-Type: application/json" \
    -d "{ \"FirstName\": \"Maarten\" }"

或者如果你使用的是PowerShell,运行:

Invoke-WebRequest `
    -Uri https://localhost:7230/people `
    -Method Post `
    -ContentType "application/json" `
    -Body '{"FirstName": "Maarten"}'

dotnet run 命令打印到控制台的localhost URL替换https://localhost:7230

响应应该是一个400 Bad request ,因为LastNameEmail 属性是必需的:

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Date: Fri, 03 Jun 2022 09:04:56 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "LastName": [
      "The LastName field is required."
    ],
    "Email": [
      "The Email field is required."
    ]
  }
}

在你确认端点工作后,你将把这个请求转换为一个测试!

测试不同的有效载荷和HTTP方法

你的Minimal API现在有一个/people 端点。它有两种可能的响应类型:一种是返回字符串值的200 OK ,另一种是以JSON有效载荷返回问题细节的400 Bad Request

MyMinimalApi.Tests项目中,添加一个PeopleTests.cs文件,包含以下代码:

using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;

namespace MyMinimalApi.Tests;

public class PeopleTests
{
    [Fact]
    public async Task CreatePerson()
    {
    }
  
    [Fact]
    public async Task CreatePersonValidatesObject()
    {
    }
}

PeopleTests 类现在包含2个你需要实现的测试方法:

  • CreatePerson() 来测试 场景200 OK
  • CreatePersonValidatesObject() 测试 情景400 Bad Request

你将从CreatePerson() 测试方法开始。该测试将再次利用WebApplicationFactory<Program> ,创建一个内存中的HTTP客户端,你可以用它来验证API:

[Fact]
public async Task CreatePerson()
{
    await using var application = new WebApplicationFactory<Program>();

    var client = application.CreateClient();
}

接下来,你将使用client ,向/people 端点发送一个JSON有效载荷。你可以使用PostAsJsonAsync() 方法向被测试的Minimal API发送JSON有效载荷。最后,你可以使用xUnitAssert 类来验证响应状态代码和响应内容。

像下面这样更新CreatePerson() 测试:

[Fact]
public async Task CreatePerson()
{
    await using var application = new WebApplicationFactory<Program>();

    var client = application.CreateClient();
  
    var result = await client.PostAsJsonAsync("/people", new Person
    {
        FirstName = "Maarten",
        LastName = "Balliauw",
        Email = "maarten@jetbrains.com"
    });
  
    Assert.Equal(HttpStatusCode.OK, result.StatusCode);
    Assert.Equal("\"Maarten Balliauw created.\"", await result.Content.ReadAsStringAsync());
}

你可以使用.NET CLI运行该测试,并确认你的Minimal API按预期工作:

dotnet test

接下来是CreatePersonValidatesObject() 测试。与CreatePerson() 测试方法一样,你将首先向内存中的Minimal API创建一个请求。只是这一次,你将发送一个空的Person 对象。

由于它的所有属性都是null 或空的,测试应该得到一个400 Bad Request 。你可以断言这确实是事实。更重要的是,你还可以使用result.Content.ReadFromJsonAsync<>() 方法来反序列化验证问题,并验证它们是否符合预期。

像下面这样更新CreatePersonValidatesObject() 测试:

[Fact]
public async Task CreatePersonValidatesObject()
{
    await using var application = new WebApplicationFactory<Program>();

    var client = application.CreateClient();
  
    var result = await client.PostAsJsonAsync("/people", new Person());

    Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
  
    var validationResult = await result.Content.ReadFromJsonAsync<HttpValidationProblemDetails>();
    Assert.NotNull(validationResult);
    Assert.Equal("The FirstName field is required.", validationResult!.Errors["FirstName"][0]);
}

我将把其他属性的验证作为一个练习留给你。

再次,尝试使用.NET CLI运行这个测试,并确认你的Minimal API如期工作:

dotnet test

干得好!你现在已经写好了测试,可以验证由你的Minimal API接受和返回的JSON有效载荷。

定制服务集合

还有一件事...你创建的Minimal API包含一个PeopleService ,在一个更现实的项目中,可能需要一个数据库连接。这对某些测试来说是可以的,而对其他测试来说是不必要的。

到目前为止,你所写的测试都是在验证Minimal API的响应。没有必要使用IPeopleService 的 "真实 "实现,所以让我们看看如何用测试实现来替换它!

MyMinimalApi.Tests项目中,创建一个新文件TestPeopleService.cs,代码如下:

public class TestPeopleService : IPeopleService
{
   public string Create(Person person) => "It works!";
}

TestPeopleService 类实现了IPeopleService ,就像真正的实现一样,但Create 方法返回一个简单的string 值。

接下来,你将更新测试方法,为WebApplicationFactory<Program> ,为IPeopleService ,配置一个服务覆盖,把它连接到TestPeopleService 。你可以通过多种方式做到这一点:使用WithWebHostBuilder()ConfigureServices() 方法,或者通过实现一个自定义的WebApplicationFactory<T> 。在本教程中,你将使用第一种方法,将IPeopleService 改为TestPeopleService

用以下代码更新CreatePerson 测试:

[Fact]
public async Task CreatePerson()
{
   await using var application = new WebApplicationFactory<Program>()
       .WithWebHostBuilder(builder => builder
           .ConfigureServices(services =>
           {
               services.AddScoped<IPeopleService, TestPeopleService>();
           }));

   var client = application.CreateClient();
  
   var result = await client.PostAsJsonAsync("/people", new Person
   {
       FirstName = "Maarten",
       LastName = "Balliauw",
       Email = "maarten@jetbrains.com"
   });
  
   Assert.Equal(HttpStatusCode.OK, result.StatusCode);
   Assert.Equal("\"It works!\"", await result.Content.ReadAsStringAsync());
}

要使用services.AddScoped ,在文件顶部的using语句中添加using Microsoft.Extensions.DependencyInjection;

注意,在代码示例中,最后的Assert.Equal 现在是测试由TestPeopleService 返回的string

根据你想对测试中的Minimal API进行多少定制,你可以将WithWebHostBuilder()ConfigureServices() 方法移出,并重写WebApplicationFactory<T> 类。这样做的好处是你可以在一个地方定制服务集合。

例如,你可以创建一个TestingApplication 类并覆盖CreateHost 方法来定制服务集合:

class TestingApplication : WebApplicationFactory<Person>
{
   protected override IHost CreateHost(IHostBuilder builder)
   {
       builder.ConfigureServices(services =>
       {
           services.AddScoped<IPeopleService, TestPeopleService>();
       });

       return base.CreateHost(builder);
   }
}

你可以在测试中使用它,用new TestingApplication() 替换new WebApplicationFactory<Program>:

[Fact]
public async Task CreatePerson()
{
   await using var application = new TestingApplication();

   var client = application.CreateClient();
  
   var result = await client.PostAsJsonAsync("/people", new Person
   {
       FirstName = "Maarten",
       LastName = "Balliauw",
       Email = "maarten@jetbrains.com"
   });
  
   Assert.Equal(HttpStatusCode.OK, result.StatusCode);
   Assert.Equal("\"It works!\"", await result.Content.ReadAsStringAsync());
}

如果你想在测试过程中开始定制Minimal API,请确保探索WebApplicationFactory<T> 的各种方法,你可以覆盖这些方法来为你正在编写的测试配置你的应用程序。

总结

就这样吧!你刚刚为ASP.NET Core Minimal API建立了几个测试,并验证了它的行为符合预期。你从测试一个返回字符串的基本端点开始,然后看到如何在请求和响应中使用不同的HTTP方法和有效载荷。你甚至为你的测试定制了ASP.NET Core服务集,并提供了自定义服务。

无论你是在写单元测试、集成测试还是两者兼而有之,你现在应该对如何使用测试服务器和为许多场景定制服务集合有了很好的理解。