测试通常从我们希望发生的事情开始。在我的上一篇文章中,我演示了如何在处理“愉快路径”场景(即,测试成功登录尝试的结果)时虚拟化您依赖的服务。但我们都知道软件会以惊人且意想不到的方式失败。现在是时候仔细看看如何处理“不太愉快的路径”了:当有人尝试使用错误的凭据登录时会发生什么?
在上面链接的第一篇文章中,我介绍了构建用户身份验证模块的过程。(现在是回顾该代码并使其运行的好时机。)此模块没有完成所有繁重的工作;它主要依赖于另一个服务来完成那些更艰巨的任务——启用用户注册、存储用户帐户和验证用户。该模块只会向此附加服务的端点发送 HTTP POST 请求;在本例中,是 /api/v1/users/login。
如果您依赖的服务尚未构建,您会怎么做?这种情况会造成阻塞。在之前的文章中,我探讨了如何使用 mountebank(一个强大的测试环境)启用的服务虚拟化来消除这种阻塞。
本文介绍了在用户重复尝试登录的情况下启用用户身份验证处理所需的步骤。第三方身份验证服务仅允许三次登录尝试,之后将停止为来自冒犯性域的 HTTP 请求提供服务。
如何模拟重复请求
Mountebank 可以非常轻松地模拟一个服务,该服务监听网络端口,匹配请求中定义的方法和路径,然后通过发回 HTTP 响应来处理它。要继续学习,请务必像我们在之前的文章中所做的那样运行 mountebank。正如我在那里解释的那样,这些值声明为 JSON,并发布到 http://localhost:2525/imposters,mountebank 用于处理身份验证请求的端点。
但现在的挑战是如何模拟 HTTP 请求不断从同一域命中同一端点的情况。这对于模拟用户提交无效凭据(用户名和密码),被告知凭据无效,尝试不同的凭据,并被反复拒绝(或愚蠢地尝试使用之前尝试失败的相同凭据登录)是必要的。最终(在本例中,在第三次失败尝试之后),用户将被禁止进行其他尝试。
编写可执行代码来模拟这种情况将必须对非常复杂的处理进行建模。但是,当使用 mountebank 时,这种类型的模拟处理非常容易完成。它通过创建响应的滚动缓冲区来完成,并且 mountebank 按缓冲区创建的顺序响应 。以下是在 mountebank 中模拟重复请求的一种方法示例
{
"port": 3001,
"protocol": "http",
"name": "authentication imposter",
"stubs": [
{
"predicates": [
{
"equals": {
"method": "post",
"path": "/api/v1/users/login"
}
}
],
"responses": [
{
"is": {
"statusCode": 200,
"body": "Successfully logged in."
}
},
{
"is": {
"statusCode": 400,
"body": "Incorrect login. You have 2 more attempts left."
}
},
{
"is": {
"statusCode": 400,
"body": "Incorrect login. You have 1 more attempt left."
}
},
{
"is": {
"statusCode": 400,
"body": "Incorrect login. You have no more attempts left."
}
}
]
}
]
}
滚动缓冲区只是 JSON 响应的无限集合,其中每个响应都用两个键值对表示:statusCode 和 body。在本例中,定义了四个响应。第一个响应是愉快路径(即,用户成功登录),其余三个响应代表失败的用例(即,错误的凭据导致状态代码 400 和相应的错误消息)。
如何测试重复请求
按如下方式修改测试
using System;
using Xunit;
using app;
namespace tests
{
public class UnitTest1
{
Authenticate auth = new Authenticate();
[Fact]
public void SuccessfulLogin()
{
var given = "valid credentials";
var expected = " Successfully logged in.";
var actual= auth.Login(given);
Assert.Equal(expected, actual);
}
[Fact]
public void FirstFailedLogin()
{
var given = "invalid credentials";
var expected = "Incorrect login. You have 2 more attempts left.";
var actual = auth.Login(given);
Assert.Equal(expected, actual);
}
[Fact]
public void SecondFailedLogin()
{
var given = “invalid credentials";
var expected = "Incorrect login. You have 1 more attempt left.";
var actual = auth.Login(given);
Assert.Equal(expected, actual);
}
[Fact]
public void ThirdFailedLogin()
{
var given = " invalid credentials";
var expected = "Incorrect login. You have no more attempts left.";
var actual = auth.Login(given);
Assert.Equal(expected, actual);
}
}
}
现在,运行测试以确认您的代码仍然有效

哇!现在所有测试都失败了。为什么?
如果您仔细查看,您会看到一个揭示性的模式

请注意,ThirdFailedLogin 首先执行,然后是 SuccessfulLogin,然后是 FirstFailedLogin,然后是 SecondFailedLogin。这是怎么回事?为什么第三个测试在第一个测试之前运行?
测试框架(xUnit)并行执行所有测试,并且执行顺序是不可预测的。您需要按顺序运行测试,这意味着您不能使用普通的 xUnit 工具包测试这些场景。
如何按正确的顺序运行测试
要强制您的测试以您定义的特定顺序运行(而不是以不可预测的顺序运行),您需要使用 NuGet Xunit.Extensions.Ordering 包扩展普通的 xUnit 工具包。在命令行中使用以下命令安装该包
$ dotnet add package Xunit.Extensions.Ordering --version 1.4.5
或将其添加到您的 tests.csproj 配置文件中
<PackageReference Include="Xunit.Extensions.Ordering" Version="1.4.5" />
一旦完成这些操作,请对您的 ./tests/UnitTests1.cs 文件进行一些修改。在您的 UnitTests1.cs 文件的开头添加以下四行
using Xunit.Extensions.Ordering;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: TestCaseOrderer("Xunit.Extensions.Ordering.TestCaseOrderer", "Xunit.Extensions.Ordering")]
[assembly: TestCollectionOrderer("Xunit.Extensions.Ordering.CollectionOrderer", "Xunit.Extensions.Ordering")]
现在您可以指定您希望测试运行的顺序。最初,通过使用以下内容注释测试来模拟愉快路径(即 SuccessfulLogin())
[Fact, Order(1)]
public void SuccessfulLogin() {
在您测试成功登录后,测试第一次失败登录
[Fact, Order(2)]
public void FirstFailedLogin()
等等。您可以通过简单地将 Order(x)(其中 x 表示您希望测试运行的顺序)注释添加到您的 Fact 来添加测试运行的顺序。
此注释保证您的测试将以您希望它们运行的确切顺序运行,现在您可以(终于!)完全测试您的集成场景。
您的测试的最终版本是
using System;
using Xunit;
using app;
using Xunit.Extensions.Ordering;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: TestCaseOrderer("Xunit.Extensions.Ordering.TestCaseOrderer", "Xunit.Extensions.Ordering")]
[assembly: TestCollectionOrderer("Xunit.Extensions.Ordering.CollectionOrderer", "Xunit.Extensions.Ordering")]
namespace tests
{
public class UnitTest1
{
Authenticate auth = new Authenticate();
[Fact, Order(1)]
public void SuccessfulLogin()
{
var given = "elon_musk@tesla.com";
var expected = "Successfully logged in.";
var actual= auth.Login(given);
Assert.Equal(expected, actual);
}
[Fact, Order(2)]
public void FirstFailedLogin()
{
var given = "mickey@tesla.com";
var expected = "Incorrect login. You have 2 more attempts left.";
var actual = auth.Login(given);
Assert.Equal(expected, actual);
}
[Fact, Order(3)]
public void SecondFailedLogin()
{
var given = "mickey@tesla.com";
var expected = "Incorrect login. You have 1 more attempt left.";
var actual = auth.Login(given);
Assert.Equal(expected, actual);
}
[Fact, Order(4)]
public void ThirdFailedLogin()
{
var given = "mickey@tesla.com";
var expected = "Incorrect login. You have no more attempts left.";
var actual = auth.Login(given);
Assert.Equal(expected, actual);
}
}
}
再次运行测试——一切都通过了!

您到底在测试什么?
本文重点介绍了测试驱动开发 (TDD),但让我们从另一种方法论,极限编程 (XP) 重新审视它。XP 定义了两种类型的测试
- 程序员测试
- 客户测试
到目前为止,在本系列关于 TDD 的文章中,我一直关注第一种类型的测试(即,程序员测试)。在本文和上一篇文章中,我转换了视角来检查进行客户测试的最有效方法。
重要的一点是,程序员(或生产者)测试侧重于精确工作。我们通常将这些精确测试称为“微测试”,而其他人可能称其为“单元测试”。另一方面,客户测试更侧重于更大的图景;我们有时将它们称为“近似测试”或“端到端测试”。
结论
本文演示了如何编写一组近似测试,这些测试集成了几个离散步骤,并确保代码可以处理所有边缘情况,包括模拟客户在重复尝试登录但未能获得必要权限时的体验。TDD 和 xUnit 以及 mountebank 等工具的这种组合可以带来经过良好测试的,从而更可靠的应用程序开发。
在未来的文章中,我将研究 mountebank 在编写客户(或近似)测试方面的其他用途。
3 条评论