前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《ASP.NET Core 与 RESTful API 开发实战》-- (第9章)-- 读书笔记(下)

《ASP.NET Core 与 RESTful API 开发实战》-- (第9章)-- 读书笔记(下)

作者头像
郑子铭
发布2021-01-13 15:47:24
7080
发布2021-01-13 15:47:24
举报

集成测试

集成测试能够确保应用程序的组件正常工作,包括应用程序支持的基础结构,如数据库和文件系统等

进行集成测试时,应为项目添加 Microsoft.AspNetCore.MvcTesting 包

它提供了 WebApplicationFactory 类,用于创建内存中的测试服务器,其定义和主要成员如下:

代码语言:javascript
复制
public class WebApplicationFactory<TEntryPoint> : IDisposable where TEntryPoint : class
{
    public TestServer Server
    {
      get
      {
        this.EnsureServer();
        return this._server;
      }
    }
    
    public IReadOnlyList<WebApplicationFactory<TEntryPoint>> Factories
    {
      get
      {
        return (IReadOnlyList<WebApplicationFactory<TEntryPoint>>) this._derivedFactories.AsReadOnly();
      }
    }
    
    public WebApplicationFactoryClientOptions ClientOptions { get; private set; } = new WebApplicationFactoryClientOptions();
    
    public HttpClient CreateClient()
    {
      return this.CreateClient(this.ClientOptions);
    }
    
    public HttpClient CreateClient(WebApplicationFactoryClientOptions options)
    {
      return this.CreateDefaultClient(options.BaseAddress, options.CreateHandlers());
    }
    
    protected virtual void ConfigureClient(HttpClient client)
    {
      if (client == null)
        throw new ArgumentNullException(nameof (client));
      client.BaseAddress = new Uri("http://localhost");
    }
    
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
    this._configuration(builder);
    }
    
    protected override TestServer CreateServer(IWebHostBuilder builder)
    {
    return this._createServer(builder);
    }
    
    protected override IWebHostBuilder CreateWebHostBuilder()
    {
    return this._createWebHostBuilder();
    }
    
    protected override IEnumerable<Assembly> GetTestAssemblies()
    {
    return this._getTestAssemblies();
    }
}

WebApplicationFactory 的泛型参数 TEntryPoint 表示被测试应用程序的入口,通常为 startup 类

WebApplicationFactory 的 CreateClient 方法能够创建 HttpClient 对象,在测试方法中,正是通过 HttpClient 对象所提供的方法对接口进行请求来完成测试

为了方便测试,xUnit 提供了 IClassFixture 接口,该接口并未包含任何成员,主要目的是标识一个类为测试类,并为测试类提供所需要的依赖

在测试项目中添加一个类 AuthorController_IntegrationTests,该类主要包含了针对 AuthorController 中各个方法的集成测试

代码语言:javascript
复制
namespace Library.API.Testing
{
    public class AuthorController_IntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
    {
        private readonly WebApplicationFactory<Startup> _factory;

        public AuthorController_IntegrationTests(WebApplicationFactory<Startup> factory)
        {
            _factory = factory;
        }
    }
}

AuthorController_IntegrationTests 构造函数的 factory 参数将会在该类实例时由 xUnit 自动构建并注入

下面是对 AuthorController 中 GetAuthorByIdAsync 方法的测试

代码语言:javascript
复制
[Theory]
[InlineData("6e51f1e7-4465-43c6-9c72-e5f2736fbe19")]
[InlineData("2faa406d-bb28-4832-aa76-d71f70470f6e")]
public async Task Test_GetAuthorById(string authorId)
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync($"api/authors/{authorId}");

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
    Assert.Contains(authorId, await response.Content.ReadAsStringAsync());
}

下面的测试方法分别验证了请求不存在资源时是否返回 404 Not Found 状态码,以及当请求一个格式不正确的资源 Id 时是否返回 400 Bad Request 状态码

代码语言:javascript
复制
[Fact]
public async Task Test_GetAuthorByNotExistId()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync($"api/authors/{Guid.NewGuid()}");

    // Assert
    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Theory]
[InlineData("a")]
[InlineData("12")]
public async Task Test_GetAuthorByNotInvalidId(string authorId)
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync($"api/authors/{authorId}");

    // Assert
    Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

到目前为止,所有测试的接口均不需要认证,而对于涉及认证的接口,需要在数据准备阶段完成必要的操作,如获取 Bearer Token 等

下面的测试方法首先验证了当客户端不指定认证信息时,是否返回 401 Not Authorized 状态码

代码语言:javascript
复制
[Fact]
public async Task Test_CreateAuthor_Unauthorized()
{
    // Arrange
    var client = _factory.CreateClient();
    var authorDto = new AuthorDto
    {
        Name = "Test Author",
        Email = "author_testing@xxx.com",
        Age = 50
    };
    var jsonContent = JsonConvert.SerializeObject(authorDto);

    // Act
    var response = await client.PostAsync("api/authors", new StringContent(content: jsonContent));

    // Assert
    Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

上面对创建作者的接口进行了测试,执行测试之前,请确保已经为该接口添加了 [Authorize] 特性

如果要获取一个 Bearer Token,则需要以 POST 方式请求 author/token 或 author/token2,并在请求时提供用户名和密码

因此首先在 AuthorController_IntegrationTests 中添加一个 LoginUser 对象,并在构造函数中将其实例化

代码语言:javascript
复制
private readonly WebApplicationFactory<Startup> _factory;
private readonly LoginUser _loginUser;

public AuthorController_IntegrationTests(WebApplicationFactory<Startup> factory)
{
    _factory = factory;
    _loginUser = new LoginUser
    {
        UserName = "demouser",
        Password = "demopassword"
    };
}

接下来为 HttpClient 添加扩展方法 TryGetBearerTokenAsync,用于为指定的用户获取 BearerToken

代码语言:javascript
复制
public static class HttpClientExtensions
{
    public static async Task<(bool result, string token)> TryGetBearerTokenAsync(this HttpClient httpClient,
        LoginUser loginUser)
    {
        var userCredentialInfo = new StringContent(
            content: JsonConvert.SerializeObject(loginUser),
            encoding: Encoding.UTF8,
            mediaType: "application/json");
        var response = await httpClient.PostAsync("auth/token", userCredentialInfo);
        var tokenResult = await response.Content.ReadAsAsync<TokenResult>();
        if (tokenResult == null)
        {
            return (false, null);
        }
        else
        {
            return (true, tokenResult.Token);
        }
    }
}

public class TokenResult
{
    public DateTimeOffset Expiration { get; set; }
    public string Token { get; set; }
}

接下来添加对 CreateAuthor 接口的正常测试,在调用 HttpClient 对象的 PostAsync 方法之前在请求中添加对 Authorization 消息头,并使它的值为 Bearer<bearer_token>

代码语言:javascript
复制
[Fact]
public async Task Test_CreateAuthor()
{
    // Arrange
    var client = _factory.CreateClient();
    var authorDto = new AuthorDto
    {
        Name = "Test Author",
        Email = "author_testing@xx.com",
        Age = 50
    };

    var jsonContent = JsonConvert.SerializeObject(authorDto);
    var bearerResult = await client.TryGetBearerTokenAsync(_loginUser);
    if (!bearerResult.result)
    {
        throw new Exception("Authentication failed");
    }

    client.DefaultRequestHeaders.Add(HeaderNames.Authorization, $"Bearer {bearerResult.token}");

    // Act
    var response = await client.PostAsync("api/authors",
        new StringContent(content: jsonContent, encoding: Encoding.UTF8, mediaType: "application/json"));

    // Assert
    Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}

WebApplicationFactory 对象会使 WebHost 与实际生产环境完全一致,然而为了确保测试方法不影响生产环境,需要使用测试数据库

WebApplicationFactory 类中提供了几个 virtual 类型的方法,如 CreateWebHostBuilder 和 ConfigureWebHost 等,方便在派生类中对这些方法进行重写,以实现自定义的逻辑

创建 CustomWebApplicationFactory 类,重写 ConfigureWebHost 方法

代码语言:javascript
复制
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();

            services.AddDbContext<LibraryDbContext>(options =>
            {
                options.UseInMemoryDatabase("LibraryTestingDb");
                options.UseInternalServiceProvider(serviceProvider);
            });

            var sp = services.BuildServiceProvider();
            using (var scope = sp.CreateScope())
            {
                var scopedServices = scope.ServiceProvider;
                var db = scopedServices.GetRequiredService<LibraryDbContext>();
                db.Database.EnsureCreated();
            }
        });
    }
}

接下来,只需要修改 AuthorController_IntegrationTests 类实现的接口为 IClassFixture<CustomWebApplicationFactory> 即可

代码语言:javascript
复制
public class AuthorController_IntegrationTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
    public AuthorController_IntegrationTests(CustomWebApplicationFactory<Startup> factory)
    {
        。。。
    }
}

再次运行该类中的所有测试方法,所有的操作数据都是 EF Core 所创建的内存数据库

9.2 文档

Swagger,也称 OpenAPI,是一个与语言无关的规范,被广泛用于实现 API 文档化,它能够描述 RESTful API,并为 API 生成人与计算机都容易理解的文档

安装

代码语言:javascript
复制
Install-Package Swashbuckle.AspNetCore

接下来,在 Startup 类的 ConfigureServices 方法中添加 Swagger 生成器

代码语言:javascript
复制
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Library API",
        Version = "v1"
    });
});

在 Configure 方法中添加 Swagger 中间件和 SaggerUI 中间件

代码语言:javascript
复制
app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Library API V1");
});
app.UseMvc();

运行程序,访问 https://localhost:5001/swagger/v1/swagger.json

该页面会显示 Swagger 生成的 JSON 文档

访问 https://localhost:5001/swagger 可以看到 SwaggerUI,它是 Swagger 文档更友好的展示方式

如果不希望在文档中展示某个 Controller 或其中某个 Action,可以添加 [ApiExplorerSettings] 特性,将 IgnoreApi 属性设置为 true

代码语言:javascript
复制
[ApiExplorerSettings(IgnoreApi = true)]

Swagger UI 默认的 URL 是 http:///swagger,如果想改变其 URL,可以修改 RoutePrefix 属性,默认为 swagger

代码语言:javascript
复制
app.UseSwaggerUI(c =>
{
    c.RoutePrefix = string.Empty;
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Library API V1");
});

Swagger 文档能够包含在代码中的 XML 注释,这会进一步增加 Swagger 文档的可读性

在项目属性窗口中的”生成“页上勾选”XML文档文件“来启用自动生成 XML 注释文档功能

为了使 Swagger 文档能够更详细地显示接口的意义,应尽可能地为 Controller 以及其中的 Action 添加描述其功能的 XML 注释

接下来,修改 ConfigureService 方法,使 Swagger 文档中包含 XML 注释文档的内容

代码语言:javascript
复制
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Library API",
        Version = "v1"
    });

    var xmlFile = Path.ChangeExtension(typeof(Startup).Assembly.Location, ".xml");
    c.IncludeXmlComments(xmlFile);
});

下例为 CreateAuthorAsync 方法添加 XML 注释

代码语言:javascript
复制
/// <summary>
/// 添加一个作者
/// </summary>
/// <param name="authorForCreationDto">作者</param>
/// <remarks>
/// 添加作者的要求:
///
///     POST api/authors
///     {
///         "name" : "Author1",
///         "Email" : "xxx@xxx.com"
///     }
/// </remarks>
/// <returns>添加结果</returns>
/// <response code="201">返回新创建的资源</response>
/// <reaponse code="400">提交请求时的信息不正确</reaponse>
[HttpPost(Name = nameof(CreateAuthorAsync))]
[ProducesResponseType(201,Type = typeof(AuthorDto))]
[ProducesResponseType(400, Type = typeof(void))]
public async Task<IActionResult> CreateAuthorAsync(AuthorForCreationDto authorForCreationDto)
{
    。。。
}

除了手动使用 [ProducesResponseType] 特性列出所有可能返回的状态码外,ASP.NET.Core 还提供了 Web API 约定

代码语言:javascript
复制
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))]

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-08-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 9.2 文档
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档