​AuthTests​​ 类中的测试检查安全终结点是否:

  • 将未经身份验证的用户重定向到应用的登录页面。
  • 为经过身份验证的用户返回内容。

在 SUT 中,​​/SecurePage​​​ 页面使用 ​​AuthorizePage​​​ 约定,将 ​​AuthorizeFilter​​​ 应用到页面。 有关详细信息,请参阅 ​​Razor Pages 授权约定​​。

C#复制

services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});

在 ​​Get_SecurePageRedirectsAnUnauthenticatedUser​​​ 测试中,通过将 ​​AllowAutoRedirect​​​ 设置为 ​​false​​​,将 ​​WebApplicationFactoryClientOptions​​ 设置为不允许重定向:

C#复制

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});

// Act
var response = await client.GetAsync("/SecurePage");

// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}

通过禁止客户端追随重定向,可以执行以下检查:

  • 可以根据预期​​HttpStatusCode.Redirect​​​ 结果检查 SUT 返回的状态代码,而不是在重定向到登录页面之后的最终状态代码(这会是​​HttpStatusCode.OK​​)。
  • 检查响应标头中的​​Location​​​ 标头值,以确认它以​​http://localhost/Identity/Account/Login​​​ 开头,而不是最终登录页面响应(其中​​Location​​ 标头不存在)。

测试应用可以在 ​​ConfigureTestServices​​​ 中模拟 ​​AuthenticationHandler<TOptions>​​​,以便测试身份验证和授权的各个方面。 最小方案返回 ​​AuthenticateResult.Success​​:

C#复制

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");

var result = AuthenticateResult.Success(ticket);

return Task.FromResult(result);
}
}

当身份验证方案设置为 ​​Test​​​(其中为 ​​ConfigureTestServices​​​ 注册了 ​​AddAuthentication​​​)时,会调用 ​​TestAuthHandler​​​ 以对用户进行身份验证。 ​​Test​​ 架构必须与应用所需的架构匹配,这一点很重要。 否则,身份验证将不起作用。

C#复制

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => {});
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});

client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test");

//Act
var response = await client.GetAsync("/SecurePage");

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

有关 ​​WebApplicationFactoryClientOptions​​​ 的详细信息,请参阅​​客户端选项​​部分。

设置环境

默认情况下,SUT 的主机和应用环境配置为使用开发环境。 使用 ​​IHostBuilder​​ 时替代 SUT 的环境:

  • 设置​​ASPNETCORE_ENVIRONMENT​​​ 环境变量(例如,​​Staging​​​、​​Production​​​ 或其他自定义值,例如​​Testing​​)。
  • 在测试应用中替代​​CreateHostBuilder​​​,以读取以​​ASPNETCORE​​ 为前缀的环境变量。

C#复制

protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));

如果 SUT 使用 Web 主机 (​​IWebHostBuilder​​​),则替代 ​​CreateWebHostBuilder​​:

C#复制

protected override IWebHostBuilder CreateWebHostBuilder() =>
base.CreateWebHostBuilder().UseEnvironment("Testing");

测试基础结构如何推断应用内容根路径

​WebApplicationFactory​​​ 构造函数通过在包含集成测试的程序集中搜索键等于 ​​TEntryPoint​​​ 程序集 ​​System.Reflection.Assembly.FullName​​​ 的 ​​WebApplicationFactoryContentRootAttribute​​​,来推断应用​​内容根​​​路径。 如果找不到具有正确键的属性,则 ​​WebApplicationFactory​​​ 会回退到搜索解决方案文件 (.sln) 并将 ​​TEntryPoint​​ 程序集名称追加到解决方案目录。 应用根目录(内容根路径)用于发现视图和内容文件。

禁用卷影复制

卷影复制会导致在与输出目录不同的目录中执行测试。 如果测试需要加载相对于 ​​Assembly.Location​​ 的文件,而你遇到问题,那么你可能需要禁用卷影复制。

若要在使用 xUnit 时禁用卷影复制,请通过​​正确的配置设置​​​在测试项目目录中创建 ​​xunit.runner.json​​ 文件:

JSON复制

{
"shadowCopy": false
}

对象的处置

执行 ​​IClassFixture​​​ 实现的测试之后,当 xUnit 处置 时,​​TestServer​​​ 和 ​​WebApplicationFactory​​​​HttpClient​​​ 会进行处置。 如果开发者实例化的对象需要处置,请在 ​​IClassFixture​​​ 实现中处置它们。 有关详细信息,请参阅​​实现 Dispose 方法​​。

集成测试示例

​示例应用​​包含两个应用:

应用

项目目录

描述

消息应用 (SUT)

​src/RazorPagesProject​

允许用户添加消息、删除一个消息、删除所有消息和分析消息。

测试应用

​tests/RazorPagesProject.Tests​

用于集成测试 SUT。

可使用 IDE 的内置测试功能(例如 ​​Visual Studio​​​)运行测试。 如果使用 ​​Visual Studio Code​​​ 或命令行,请在 ​​tests/RazorPagesProject.Tests​​ 目录中的命令提示符处执行以下命令:

控制台复制

dotnet test

消息应用 (SUT) 组织

SUT 是具有以下特征的 Razor Pages 消息系统:

  • 应用的索引页面(​​Pages/Index.cshtml​​​ 和​​Pages/Index.cshtml.cs​​)提供 UI 和页面模型方法,用于控制添加、删除和分析消息(每个消息的平均字词数)。
  • 消息由​​Message​​​ 类 (​​Data/Message.cs​​​) 描述,并具有两个属性:​​Id​​​(键)和​​Text​​​(消息)。​​Text​​ 属性是必需的,并限制为 200 个字符。
  • 消息使用​​实体框架的内存中数据库​​†存储。
  • 应用在其数据库上下文类​​AppDbContext​​​ (​​Data/AppDbContext.cs​​) 中包含数据访问层 (DAL)。
  • 如果应用启动时数据库为空,则消息存储初始化为三条消息。
  • 应用包含只能由经过身份验证的用户访问的​​/SecurePage​​。

†EF 主题​​使用 InMemory 进行测试​​​说明如何将内存中数据库用于 MSTest 测试。 本主题使用 ​​xUnit​​ 测试框架。 不同测试框架中的测试概念和测试实现相似,但不完全相同。

尽管应用未使用存储库模式且不是​​工作单元 (UoW) 模式​​​的有效示例,但 Razor Pages 支持这些开发模式。 有关详细信息,请参阅​​设计基础结构持久性层​​​和​​测试控制器逻辑​​(该示例实现存储库模式)。

测试应用组织

测试应用是 ​​tests/RazorPagesProject.Tests​​ 目录中的控制台应用。

测试应用目录

描述

​AuthTests​

包含针对以下方面的测试方法:


  • 未经身份验证的用户访问安全页面。
  • 经过身份验证的用户访问安全页面(通过模拟​​AuthenticationHandler<TOptions>​​)。
  • 获取 GitHub 用户配置文件,并检查配置文件的用户登录。

​BasicTests​

包含用于路由和内容类型的测试方法。

​IntegrationTests​

包含使用自定义 ​​WebApplicationFactory​​ 类的索引页面的集成测试。

​Helpers/Utilities​

  • ​Utilities.cs​​​ 包含用于通过测试数据设定数据库种子的​​InitializeDbForTests​​ 方法。
  • ​HtmlHelpers.cs​​​ 提供了一种方法,用于返回 AngleSharp​​IHtmlDocument​​ 供测试方法使用。
  • ​HttpClientExtensions.cs​​​ 为​​SendAsync​​ 提供重载,以将请求提交到 SUT。

测试框架为 ​​xUnit​​​。 使用 ​​Microsoft.AspNetCore.TestHost​​​(包含 ​​TestServer​​​)进行集成测试。 由于 ​​Microsoft.AspNetCore.Mvc.Testing​​​ 包用于配置测试主机和测试服务器,因此 ​​TestHost​​​ 和 ​​TestServer​​ 包在测试应用的项目文件或测试应用的开发者配置中不需要直接包引用。

集成测试在执行测试前通常需要数据库中的小型数据集。 例如,删除测试需要进行数据库记录删除,因此数据库必须至少有一个记录,删除请求才能成功。

示例应用使用 ​​Utilities.cs​​ 中的三个消息(测试在执行时可以使用它们)设定数据库种子:

C#复制

public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}

SUT 的数据库上下文在其 ​​Startup.ConfigureServices​​​ 方法中注册。 测试应用的 ​​builder.ConfigureServices​​​ 回调在执行应用的 ​​Startup.ConfigureServices​​​ 代码之后执行。 若要将不同的数据库用于测试,必须在 ​​builder.ConfigureServices​​​ 中替换应用的数据库上下文。 有关详细信息,请参阅​​自定义 WebApplicationFactory​​ 部分。

对于仍使用 ​​Web 主机​​​的 SUT,测试应用的 ​​builder.ConfigureServices​​​ 回调先于 SUT 的 ​​Startup.ConfigureServices​​​ 代码。 之后执行测试应用的 ​​builder.ConfigureTestServices​​ 回调。