集成测试

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

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

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

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 中各个方法的集成测试

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 方法的测试

[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 状态码

[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 状态码

[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 对象,并在构造函数中将其实例化

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

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>

[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 方法

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> 即可

public class AuthorController_IntegrationTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
public AuthorController_IntegrationTests(CustomWebApplicationFactory<Startup> factory)
{
。。。
}
}

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

9.2 文档

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

安装

Install-Package Swashbuckle.AspNetCore

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

services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Library API",
Version = "v1"
});
});

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

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

[ApiExplorerSettings(IgnoreApi = true)]

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

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 注释文档的内容

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 注释

/// <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 约定

[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))]

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

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

  1. 使用ASP.NET Core构建RESTful API的技术指南

    译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术标准<微软RESTFul API指南>,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之 ...

  2. 4类Storage方案(AS开发实战第四章学习笔记)

    4.1 共享参数SharedPreferences SharedPreferences按照key-value对的方式把数据保存在配置文件中,该配置文件符合XML规范,文件路径是/data/data/应 ...

  3. 菜单Menu(AS开发实战第四章学习笔记)

    4.5 菜单Menu Android的菜单主要分两种,一种是选项菜单OptionMenu,通过按菜单键或点击事件触发,另一种是上下文菜单ContextMenu,通过长按事件触发.页面的布局文件放在re ...

  4. [Android]《Android艺术开发探索》第一章读书笔记

    1. 典型情况下生命周期分析 (1)一般情况下,当当前Activity从不可见重新变为可见状态时,onRestart方法就会被调用. (2)当用户打开新的Activity或者切换到桌面的时候,回调如下 ...

  5. 温故知新,使用ASP.NET Core创建Web API,永远第一次

    ASP.NET Core简介 ASP.NET Core是一个跨平台的高性能开源框架,用于生成启用云且连接Internet的新式应用. 使用ASP.NET Core,您可以: 生成Web应用和服务.物联 ...

  6. 快读《ASP.NET Core技术内幕与项目实战》WebApi3.1:WebApi最佳实践

    本节内容,涉及到6.1-6.6(P155-182),以WebApi说明为主.主要NuGet包:无 一.创建WebApi的最佳实践,综合了RPC和Restful两种风格的特点 1 //定义Person类 ...

  7. 零基础ASP.NET Core WebAPI团队协作开发

    零基础ASP.NET Core WebAPI团队协作开发 相信大家对“前后端分离”和“微服务”这两个词应该是耳熟能详了.网上也有很多介绍这方面的文章,写的都很好.我这里提这个是因为接下来我要分享的内容 ...

  8. ASP.NET Core WebApi构建API接口服务实战演练

    一.ASP.NET Core WebApi课程介绍 人生苦短,我用.NET Core!提到Api接口,一般会想到以前用到的WebService和WCF服务,这三个技术都是用来创建服务接口,只不过Web ...

  9. 从 0 使用 SpringBoot MyBatis MySQL Redis Elasticsearch打造企业级 RESTful API 项目实战

    大家好!这是一门付费视频课程.新课优惠价 699 元,折合每小时 9 元左右,需要朋友的联系爱学啊客服 QQ:3469271680:我们每课程是明码标价的,因为如果售价为现在的 2 倍,然后打 5 折 ...

  10. Asp.Net Core 5 REST API - Step by Step

    翻译自 Mohamad Lawand 2021年1月19日的文章 <Asp.Net Core 5 Rest API Step by Step> [1] 在本文中,我们将创建一个简单的 As ...

随机推荐

  1. oppo和海康嵌入式软件工程师面经总结

    目录 海康 一面(3.23,35min) 自我介绍 项目介绍 你做的这个项目遇到了那些问题,如何解决的? 移植uboot,只做了移植吗? 用的那个文件系统? 移植过程中,网卡驱动做了那些工作? 写过那 ...

  2. freeswitch如何判断挂机方

    概述 freeswitch作为VOIP的软交换平台,需要对呼叫的信息做判断和归类. 常见的呼叫信息中,挂机方向的信息对于话单统计有很大的用处. 但是fs的原始话单和日志中并没有挂机方向的信息. 环境 ...

  3. maven 工程pom依赖优化及常用命令

    本文为博主原创,转载请注明出处: 1. mvn dependency:list ---- 列出项目的所有jar包 mvn dependency:list -Dverbose 该命令可以列出项目依赖的所 ...

  4. SpringMVC02——第一个MVC程序-注解版(high版!!!!)

    注解版 新建一个子项目,添加Web支持 在pom.xml文件中引入相关的依赖:主要引入Spring框架核心库.SpringMVC.servlet,JSTL等. 创建实体类Fruit package c ...

  5. [转帖]PG-用户|角色管理

    https://www.cnblogs.com/binliubiao/p/14415203.html 1. 角色 ​ 决定发起连接的命令的初始访问权限.在PG中,角色和用户相当于是一样的对象.区别在于 ...

  6. [转帖]人大金仓- KWR 报告初体验

    [本文正在参与炫"库"行动-人大金仓有奖征文] 开发者请集结丨炫"库"行动--2021人大金仓征文大赛悬赏万元等你来! 最近一直在研究 Oracle 的 AWR ...

  7. [转帖]如何用python连接Linux服务器

    1.安装paramiko库 pip install paramiko 2.使用paramiko库连接linux #导入库 import paramiko 创建一个sshclient对象 ssh = p ...

  8. [转帖]Springboot容器化镜像设置堆内存大小

    参考资料:Best Practices: Java Memory Arguments for Containers - DZone Java 有三种方式设置最大堆内存大小: 1. -Xmx 2. -X ...

  9. [转帖]基于腾讯云微服务引擎(TSE) ,轻松实现云上全链路灰度发布

    https://my.oschina.net/u/4587289/blog/8570699 1.  概述 软件开发过程中,应用发布非常频繁,通常情况下,开发或运维人员会将系统里所有服务同时上线,使得所 ...

  10. 往返回来的数据数组Array中添加一个字段的最优写法

    在工作中我们经常会对后端返回来的数据进行添加一个字段: 最优的写法是 直接在 res.data[i].xx=aa 这样的方式去添加: 添加好了之后美酒 可以去赋值了: 让表格去渲染数据 this.$a ...