端到端(E2E)测试移动应用可能具有挑战性,因为涉及许多变量因素。移动应用依赖于用户交互的前端界面以及处理数据的后端服务。
问题出在哪里?一个常见且棘手的问题是:问题是出现在前端还是后端,在测试时?
从我的经验来看,解决移动端端到端流程中的问题可能需要很长时间,通常需要多个团队合作——每个团队负责系统的不同部分。作为一名测试人员,这意味着你需要在不同团队间来回跑动,以找到问题的根本原因。
简化调试鉴于这些挑战,解决根本原因的复杂性可能会延迟测试。但如果我们能够从这个方程式中去掉一个变量会怎么样?
想象通过在移动应用上创建一个完全不依赖后端的端到端(E2E)流程来简化这个过程。这消除了对外部系统的依赖,加快了测试周期的速度,并使调试变得更简单。
这就好比做一次高级移动组件测试,仅仅关注移动部分,无需顾虑后台服务。
鉴于在前端和后端之间追踪问题的复杂性,更可控的测试方法变得至关重要。这就是桩服务发挥作用的地方。
介绍桩服务:替代后端系统在典型的系统架构中,移动应用与后端服务器通信,后端服务器再与平台服务进行交互以处理数据并返回响应信息。插图1 展示了这一流程,突出了涉及的多个层级。
Illustration 1
更换后端但是我们怎样才能在保持端到端(E2E)流程的同时消除整个后端系统呢?解决方案是stubbing——换句话说,用预设的响应来模拟后端行为(如图2所示)。
图2:
占位服务是干嘛的?如 图2 所示,在移动应用程序和后端之间插入了一个stub服务。该服务有效地代替了后端的功能,通过用预定义的响应来回应应用的请求,模仿后端的行为来进行回应。
这个桩拦截和处理所有通信,让我们可以在测试时模拟后端返回。
关键优势是什么?该应用运行仿佛后端是完全可用的一样,无需依赖实际的后端支持。这创造了一个流畅的测试体验,在这种体验中,移动应用完全不知道它正在与一个模拟服务进行通信。
现在我们已经介绍了 stub 服务的概念,接下来让我们深入了解它们的工作原理,以及如何高效地设置它们来进行移动应用测试。
它是怎么运作的?我们在 stub 服务中设置了一个模拟响应,以匹配来自应用程序的预期请求。此配置包括三个主要元素:
- 请求详情:stub服务需要知道它应该期待什么样的请求,包括HTTP方法和URL。
- 参数:这包括所有必要的请求参数和正文内容。
- 响应:当请求匹配时,服务应回复的预期响应。
例如,该应用会发出如下请求:
(这里原本应该有一个具体的请求示例,但没有提供具体的内容。)
查询 /books/find/123
在这种情况下,测试用服务将被设置成返回预设的响应:
{
"title": “自动化实践指南”,
"year": “2024”
}
匹配请求过程
既然我们知道应用程序的行为,我们可以预料到这次调用的到来。当 stub 服务接收到请求时,它会按照以下步骤操作:
- 匹配请求内容:服务会检查请求的URL、方法和参数是否与配置匹配。
- 返回响应:如果有匹配,则返回预定义的响应。如果没有找到匹配项,服务返回一个错误,表明没有找到对应的预定义响应。
虽然 stubbing 听起来可能很复杂,但实际上一旦基础设施准备好后,实现起来就简单多了。我们一步步来拆解它。
让我们把它拆分开来这听起来可能很让人望而却步,我同意——建立桩代码需要一定努力。然而,一旦它建立起来,实现实际的桩就会变得简单直接。
那么我们该怎么做到呢?
桩化的第一步是确保应用程序仅与桩通信。我们通过设置专门的应用程序版本来实现这一点。
一个专为特定用途设计的应用程序风格。为了做到这一点,我们创建了一个专用的应用版本,将应用中所有使用的URL替换为桩服务的地址。这样我们就作为测试人员,对应用程序通信的全面掌控。
当配置好之后,我们就继续设置一个稳固的桩服务,该服务能够处理传入请求并返回预设响应。
智能 Stub 服务这个stub服务需要能够处理来自应用的所有传入信息。为此,我们有以下需求:
- 一个接收任何传入请求的端点,不论是 REST 还是 GraphQL。
- 用于保存和配置预定义模拟响应的端点。
- 跟踪应用程序发出的每个请求。
图3 提供了桩功能工作原理的详细分解。我们现在来一步步看一个功能完整的桩(Stub)服务所需的关键组件。
图3 — 关于 stubbing 的详细说明
我们的第一步是确保桩服务能处理来自应用的通信。
接收所有传入请求的接口怎么能让stub服务监听所有传入的请求?只需要一个端点,这个端点可以接受任意类型的请求。以下是一个用Kotlin编写的REST端点的例子,它处理这种场景。
@RequestMapping(value = "/stub", method = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE])
fun handleRequest(
@RequestBody(required = false) body: String?,
request: HttpServletRequest
): ResponseEntity<String> {
// 收到 ${request.method} 请求,请求地址为 ${request.requestURI}
// 您可以选择记录请求头或其他请求信息
println("Headers: ${request.headerNames.toList()}")
println("Body: $body")
// 接下来,我们将依据配置的stub来匹配并返回响应
val response = matchStubbedResponse(request, body)
return if (response != null) {
ResponseEntity.ok(response)
} else {
ResponseEntity.status(HttpStatus.NOT_FOUND).body("没有找到匹配的stub")
}
}
一旦我们处理了传入的请求,就需要告诉stub服务它每次收到请求时如何响应。
保存和配置预定义模拟响应的端点.为了定义我们的桩,我们需要一个端点来创建并存储桩配置于数据库中,每个配置都有一个唯一ID,以便跟踪和调试。
下面是一个简化的为创建测试桩而设计的接口示例:
@RestController
class StubController(val stubService: StubService) {
@PostMapping("/stub/create")
fun createStub(@RequestBody stubRequest: StubRequest): ResponseEntity<String> {
// 将存根保存至数据库,使用唯一标识ID
val stubId = stubService.saveStub(stubRequest)
return ResponseEntity.ok("存根创建成功,其ID是:$stubId")
}
}
data class StubRequest(
val method: String,
val url: String,
val requestBody: String?,
val responseBody: String,
val responseStatus: Int
)
在管理桩(stub)时,考虑它们的生命周期是很重要的。我们希望每次请求都返回相同的结果,还是希望响应根据状态的不同而改变?例如,一个端点最初可以返回一个响应,然后随着应用程序状态的变化返回另一个响应。虽然为了更复杂的测试场景增加这种灵活性是必要的,但我们目前只关注静态桩响应,保持简单。
记录应用每个发出的请求的功能
跟踪和匹配每个发出的请求需要仔细规划并与开发团队紧密合作。关键是确保你知道应用程序在发出哪些调用以及每个调用的相关信息。
例如,当用户登录应用程序时,应用程序会生成一个每次登录都会变化的唯一会话ID。我们确保每次请求都会带上这个唯一的会话ID,这个ID会放在一个特定的头部。
这有什么作用?- 我们可以将桩匹配到特定请求或特定测试会话中的特定调用。
- 因为每个测试都有其独特的会话ID,所以桩不会互相干扰。
- 我们可以选择桩是通用的(总是返回相同的响应)还是特定的(只为该特定会话ID返回响应)。
图4 我们如何配置桩服务来处理特定测试中的特定请求。
这是图4
接下来,这里有一个修改后的代码段,在匹配存根时会考虑会话ID头。
@RequestMapping(value = "/stub", method = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE])
fun handleRequest(
@RequestBody(required = false) body: String?,
request: HttpServletRequest
): ResponseEntity<String> {
val sessionId = request.getHeader("Session-Id")
println("收到 ${request.method} 请求到 ${request.requestURI},带有 Session-Id: $sessionId")
val response = matchStubbedResponse(request, body, sessionId)
return if (response != null) {
ResponseEntity.ok(response)
} else {
ResponseEntity.status(HttpStatus.NOT_FOUND).body("没有找到对应的 Stub")
}
}
来回顾一下
我们的解决方案围绕通过无需真实后端来简化移动应用测试展开。我们通过引入一个模拟后端来实现这一点,它作为模拟后端,处理所有请求并用预定义的响应回应。
专属应用沟通我们将应用设置为仅与我们的桩服务进行通信,通过使用专用的应用程序配置将实际后端的 URL 替换成桩服务的 URL,以此来实现。
沟通处理该 stub 服务程序首先监听所有传入的请求,尝试将它们与预设的配置进行匹配,并返回相应的模拟响应。若无匹配则返回错误信息。
优化沟通匹配我们将 session ID 包含在请求中,以确保存根与特定的测试用例绑定。这使我们能够并行运行测试而不会发生冲突,确保每个存根都专门服务于各自的测试会话。
灵活性测试用的响应可以是通用的(总是给出相同的回复)或特定的(针对特定的会话ID进行定制),从而提供灵活性,根据测试场景进行调整。
通过控制应用程序的通信方式,我们排除后端作为测试中的变量,在测试中可以完全专注于移动应用程序的行为表现。
使用BrowserStack等工具规划桩脚本为了有效地规划和配置我们的桩代码,我们可以使用BrowserStack这样的工具,它提供了网络追踪工具。此工具会捕获和显示应用程序发出的每一个请求,包括请求详情和响应数据及其调用顺序。
使用此工具时,
- 我们可以轻松地观察应用程序的整个网络流量,了解哪些请求被发出以及何时发出。
- 这些信息可以帮助我们设计准确的桩设置,确保我们的模拟的回应与应用程序的真实行为一致。
- 跟踪调用的顺序在测试复杂工作流时特别有帮助,因为它使我们能够为不同阶段的具体情况创建更具体的桩。
这对于像BrowserStack这样的工具对我们来说非常宝贵,使我们能够规划和改进基于stub的测试方式。
总结此解决方案快速高效地提供了应用状态的宝贵见解,完全不用担心超时、版本冲突或环境限制。
这里概述的内容只是个开始。采用桩基方法需要仔细规划和执行,但一旦实施到位,这将为测试人员和开发人员提供一个强大的工具。
它允许你自信地找出问题的根源,消除疑虑,从而简化测试流程并提升整体效率。