服务虚拟化与测试驱动开发的关系

Mountebank 模拟您依赖的服务,以便自主团队可以继续开发活动,而无需等待任何人。
142 位读者喜欢这篇文章。
Person using a laptop

软件开发的敏捷方法依赖于服务虚拟化,以赋予每个 IT 团队自主权。这种方法消除了障碍,并允许自主团队继续开发活动,而无需等待任何人。这样,一旦团队开始迭代/冲刺,就可以开始集成测试。

自动化服务的工作原理

任何自动化服务都通过已发布的端点提供给消费者。这意味着只有当服务在线可用时,才能实现自动化。

任何希望利用可用自动化服务的消费者都必须能够通过 HTTP 协议向该服务的端点发送请求。其中一些服务在通过 HTTP 协议接收到请求后,将通过简单地发回一些数据来响应。其他服务可能会通过实际执行某些工作来响应通过 HTTP 协议接收请求。例如,服务可以创建资源(例如,创建订单)、更新资源(更新订单)或删除资源(取消订单)。

所有这些活动都通过 HTTP 协议触发。在最简单的情况下,服务消费者发起的动作是 GET(例如,HTTP GET)。该请求可能带有某些查询值;这些值将由服务用于缩小搜索范围(例如,“搜索订单号 12345 并返回数据”)。

在更复杂的情况下,请求可能带有 POST 某些值的指令;服务将接受该请求并期望与其关联一些值。这些值通常称为有效负载。当服务接受包含有效负载的 HTTP POST 请求时,它将尝试处理它。它可能成功处理,也可能不成功处理,但无论哪种方式,它都会使用状态代码和可选的状态消息响应服务消费者。这样,服务消费者将被通知其请求的成功/失败,以便他们可以决定下一步应该是什么。

什么是服务虚拟化?

现在我们了解了自动化服务的工作原理,应该更容易理解如何虚拟化它们。简而言之,可以模拟在托管站点上发布的任何服务。您可以插入一个假的、伪装的服务来模拟真实服务的行为,而不是直接向服务提供商的端点发送 HTTP 请求。

从服务消费者的角度来看,与真实服务还是虚假服务交互绝对没有区别。交互保持不变。

虚拟化一项服务

好了,说得够多了,我将卷起袖子,展示如何在实践中做到这一点。假设您的团队正在启动一个新项目,并以完整的用户故事的形式接收需求

验证用户

作为新应用程序

我想要验证用户

因为我们想确保应用程序的适当安全性

验收标准

场景 #1: 新应用程序成功验证用户

假设用户已导航到登录页面

并且用户已提交凭据

当新应用程序收到登录请求时

然后新应用程序成功验证用户

并且新应用程序显示响应消息“用户成功登录。”

场景 #2: 新应用程序无法在第一次尝试时验证用户

假设用户已导航到登录页面

并且用户已提交凭据

当新应用程序收到登录请求时

然后新应用程序无法成功验证用户

并且新应用程序显示响应消息“登录不正确。您还剩 2 次尝试机会。”

场景 #3: 新应用程序无法在第二次尝试时验证用户

假设用户已导航到登录页面

并且用户已提交凭据

当新应用程序收到登录请求时

然后新应用程序无法成功验证用户

并且新应用程序显示响应消息“登录不正确。您还剩 1 次尝试机会。”

场景 #4: 新应用程序无法在第三次尝试时验证用户

假设用户已导航到登录页面

并且用户已提交凭据

当新应用程序收到登录请求时

然后新应用程序无法成功验证用户

并且新应用程序显示响应消息“登录不正确。您没有更多尝试机会了。”

在开始处理此用户故事时,首先要做的是创建所谓的“可行性骨架”(对于本练习,我将使用标准的 .Net Core 平台加上 xUnit.net,我在之前的文章中讨论过 (从这篇开始这里有另一个例子)。请参阅它们以了解有关如何安装、配置和运行所需工具的技术细节。

通过打开命令行并键入来创建可行性骨架基础设施

mkdir AuthenticateUser

然后移动到 AuthenticateUser 文件夹内

cd AuthenticateUser

并为测试创建一个单独的文件夹

mkdir tests

移动到 tests 文件夹 (cd tests) 并启动 xUnit 框架

dotnet new xunit

现在向上移动一个文件夹(返回到 AuthenticateUser)并创建 app 文件夹

mkdir app
cd app

创建 C# 代码所需的支架

dotnet new classlib

可行性骨架现在已准备就绪!打开您选择的编辑器并开始编码。

首先编写一个失败的测试

本着 TDD 的精神,首先编写失败的测试(请参阅之前的文章,了解为什么在尝试使其通过之前看到测试失败很重要)

using System;
using Xunit;
using app;

namespace tests {
    public class UnitTest1 {
        Authenticate auth = new Authenticate();

        [Fact]
        public void SuccessLogin(){
            var given = "credentials";
            var expected = "Successful login.";
            var actual = auth.Login(given);
            Assert.Equal(expected, actual);
        }
    }
}

此测试表明,如果有人向 Authenticate 组件的 Login 方法提供一些凭据(即,秘密用户名和密码)以处理请求,则期望它返回消息“成功登录”。

当然,这是尚不存在的功能——SuccessLogin() 模块中实例化的 Authenticate 模块尚未编写。因此,您不妨继续首次尝试编写所需的功能。在 app 文件夹中创建一个新文件 (Authenticate.cs) 并添加以下代码

using System;

namespace app {
    public class Authenticate {
        public string Login(string credentials) {
            return "Not implemented";
        }
    }
}

现在,导航到 tests 文件夹并运行

dotnet test

Output of dotnet.test

测试失败,因为它期望输出“成功登录”,但实际上得到了“尚未实现”的输出。

增加第二天操作的复杂性

现在您已经创建了“愉快路径”期望并使其失败,现在是时候开始实现将使失败的测试通过的功能了。第二天,您参加站立会议并报告说您已经开始处理“验证用户”故事。您让团队知道您已经为“愉快路径”创建了第一个失败的测试,而今天的计划是实现代码以使失败的测试通过。

您解释了您首先创建一个包含 usernamepassword 和其他相关属性的 User 表的意图。但是 scrum master 打断并解释说 User 模块由另一个团队处理。重复维护用户将是不良做法,因为信息将很快失去同步。因此,您应该利用 User 团队正在开发的身份验证服务,而不是构建 User 模块(其中将包括身份验证逻辑)。

这是个好消息,因为它为您省去了编写大量代码来实现 User 处理的麻烦。受到鼓舞,您热情地宣布您将快速拼凑一个函数,该函数将获取用户凭据并将它们发送到 User 团队构建的服务。

唉,当您得知 User 团队尚未开始构建 User authentication 服务时,您的意图再次被扼杀。他们仍在将用户故事分配到待办事项列表中。您感到沮丧,接受了这样一个事实,即至少需要几天(如果不是几周?)才能开始处理 User authentication 故事。

然后 scrum master 说没有理由等待 User authentication 服务被构建和部署到测试。您可以立即开始开发身份验证功能。但是您如何做到这一点呢?

scrum master 提供了一个简单的建议:利用服务虚拟化。由于 User 模块的所有规范都已固化并签署,因此您有一个可靠的、非易失性的合同来构建您的解决方案。User 服务团队发布的合同声明,为了验证用户身份,必须满足特定期望

  1. 希望验证用户身份的客户端应向端点 http://some-domain.com/api/v1/users/login 发送 HTTP POST 请求。
  2. 发送到上述端点的 HTTP POST 必须具有包含用户凭据(即,用户名和密码)的 JSON 有效负载。
  3. 收到请求后,服务将尝试让用户登录。如果用户名和密码与记录中的信息匹配,则服务将返回 HTTP 响应,其中包含状态代码 200,响应正文包含消息“用户成功登录。”

因此,现在您知道了合同详细信息,就可以开始构建解决方案了。以下是将连接到端点、发送 HTTP POST 请求并接收 HTTP 响应的代码

using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace app {
    public class Authenticate {
        HttpClient client = new HttpClient();
        string endPoint = "http://some-domain.com/api/v1/users/login";

        public string Login(string credentials) {
            Task<string> response = CheckLogin(credentials);
            return response.Result;
        }

        private async Task<string> CheckLogin(string credentials) {
            var values = new Dictionary<string, string>{{"credentials", credentials}};
            var content = new FormUrlEncodedContent(values);
            var response = await client.PostAsync(endPoint, content);
            return await response.Content.ReadAsStringAsync();
        }
    }
}

此代码将不起作用,因为 http://some-domain.com 尚不存在(尚未)。您现在被困住了吗,等待另一个团队最终构建和部署该服务?

并非如此。服务虚拟化来救援!让我们假装该服务已经存在并继续开发。

如何虚拟化服务

虚拟化 User authentication 服务的一种方法是编写一个新的应用程序(新的 API)并在本地运行它。此 API 将镜像真实 User authentication API 指定的合同,并且只会返回硬编码的存根数据(它将是一个虚假服务)。

听起来是个好计划。同样,团队在站立会议期间反驳,质疑编写、构建、测试和部署一个全新的应用程序以完成此虚假功能的必要性。这有点不值得麻烦,因为当您交付新的虚假应用程序时,另一个团队可能已经准备好提供真实的服务。

因此,您陷入了僵局。看起来您被迫等待您的依赖项实现。您未能控制您的依赖项;您现在别无选择,只能以顺序方式工作。

没那么快!有一个很棒的新工具叫做 mountebank,非常适合虚拟化任何服务。使用此工具,您可以快速启动一个本地服务器,该服务器侦听您指定的端口并接受订单。为了使其模拟服务,您只需告诉它要侦听哪个端口以及要处理哪个协议。协议的选择是

  • HTTP
  • HTTPS
  • SMTP
  • TCP

在这种情况下,您需要 HTTP 协议。首先,安装 mountebank—如果您的计算机上安装了 npm,您只需在命令行中键入

npm install -g mountebank

安装完成后,通过键入来运行 mountebank

mb

启动时,mountebank 将显示

mountebank startup

现在您已准备好虚拟化 HTTP 服务。在这种情况下,User authentication 服务期望接收 HTTP POST 请求;以下是已实现的代码如何发送 HTTP POST 请求

var response = await client.PostAsync(endPoint, content);

您现在必须建立该 endPoint。理想情况下,所有虚拟化服务都应在 localhost 服务器中支持,以确保快速执行集成测试。

为此,您需要配置 imposter。在其最基本的形式中,imposter 是一个简单的 JSON 键值对集合,其中包含端口和协议的定义

{
    "port": 3001,
    "protocol": "http"
}

此 imposter 配置为处理 HTTP 协议并侦听端口 3001 上的传入请求。

仅侦听端口 3001 上的传入 HTTP 请求不会有太大作用。一旦请求到达该端口,就需要告诉 mountebank 如何处理该请求。换句话说,您不仅虚拟化了特定端口上服务的可用性,还虚拟化了虚拟化服务将如何响应请求的方式。

为了实现该级别的服务虚拟化,您需要告诉 mountebank 如何配置存根。每个存根由两个组件组成

  1. 谓词集合
  2. 预期响应集合

谓词(有时称为匹配器)缩小了传入请求的范围。例如,使用 HTTP 协议,您可以期望多种类型的方法(例如,GET、POST、PUT、DELETE、PATCH 等)。在大多数服务虚拟化场景中,我们有兴趣模拟特定于特定 HTTP 方法的行为。此场景是关于响应 HTTP POST 请求,因此您需要配置您的存根以仅匹配 HTTP POST 请求

{
    "port": 3001,
    "protocol": "http",
    "stubs": [
        {
            "predicates": [
                {
                    "equals": {
                        "method": "post"
                    }
                }
            ]
        }
    ]
}

此 imposter 定义了一个谓词,该谓词仅匹配(使用关键字 equals)HTTP POST 请求。

现在仔细查看 endPoint 值,如已实现的代码中所定义

string endPoint = "http://localhost:3001/api/v1/users/login";

除了侦听端口 3001(如 http://localhost:3001 中定义的那样)之外,endPoint 更具体,因为它期望传入的 HTTP POST 请求转到 /api/v1/users/login 路径。您如何告诉 mountebank 仅完全匹配 /api/v1/users/login 路径?通过将路径键值对添加到存根的谓词

{
    "port": 3001,
    "protocol": "http",
    "stubs": [
        {
            "predicates": [
                {
                    "equals": {
                        "method": "post",
                        "path": "/api/v1/users/login"
                    }
                }
            ]
        }
    ]
}

此 imposter 现在知道到达端口 3001 的 HTTP 请求必须是 POST 方法,并且必须指向 /api/v1/users/login 路径。剩下要模拟的唯一内容是预期的 HTTP 响应。

将响应添加到 JSON imposter

{
    "port": 3001,
    "protocol": "http",
    "stubs": [
        {
            "predicates": [
                {
                    "equals": {
                        "method": "post",
                        "path": "/api/v1/users/login"
                    }
                }
            ],
            "responses": [
                {
                    "is": {
                        "statusCode": 200,
                        "body": "Successful login."
                    }
                }
            ]
        }
    ]
}

使用 mountebank imposters,您可以将响应定义为 JSON 键值对的集合。在大多数情况下,只需声明响应是 statusCodebody 就足够了。这种情况正在模拟“愉快路径”响应,该响应具有状态代码 OK (200) 和包含简单消息 Successful login 的正文(如验收标准中指定)。

如何运行虚拟化服务?

好了,现在您已经虚拟化了 User authentication 服务(至少是它的“愉快路径”),您如何运行它呢?

请记住,您已经启动了 mountebank,并且它报告说它正在内存中作为 http://localhost 域运行。Mountebank 正在侦听端口 2525 并接受订单。

太棒了,现在您必须告诉 mountebank 您已准备好 imposter。您如何做到这一点?向 http://localhost:2525/imposters 发送 HTTP POST 请求。请求正文必须包含您上面创建的 JSON。有几种技术可用于发送该请求。如果您精通 curl,则使用它发送 HTTP POST 请求将是启动 imposter 最简单、最快的方法。但是许多人更喜欢更用户友好的方式向 mountebank 发送 HTTP POST。

执行此操作的简单方法是使用 Postman。如果您下载并安装 Postman,您可以将其指向 http://localhost:2525/imposters,从下拉菜单中选择 POST 方法,并将 imposter JSON 复制并粘贴到原始正文中。

当您单击“发送”时,将创建 imposter,您应该获得状态 201(已创建)。

Postman output

您的虚拟化服务现在正在运行!您可以通过导航到 tests 文件夹并运行 dotnet test 命令来验证它

dotnet test output

结论

此演示展示了通过模拟您依赖的服务来消除障碍和控制依赖项是多么容易。Mountebank 是一个出色的工具,可以轻松且廉价地模拟各种非常复杂、精细的服务。

在本期中,我只有时间说明如何虚拟化一个简单的“愉快路径”服务。如果您回到实际的用户故事,您会注意到它的验收标准包含几个“不太愉快”的路径(有人反复尝试使用无效凭据登录的情况)。正确地虚拟化和测试这些用例有点棘手,所以我将把该练习留到本系列下一期中。

您将如何使用服务虚拟化来解决您的测试需求?我很乐意在评论中听到您的想法。

接下来阅读什么
标签
User profile image.
Alex 自 1990 年以来一直从事软件开发。他目前的热情是如何将“软”带回软件中。他坚信,我们的行业已经达到了相当的成熟度,完全可以实现这个崇高的目标(即将“软”带回软件中)。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.