tl;dr:为 Java 应用程序的存根网络调用维护(生成并保持最新)固定装置的最佳方法是什么?
我想单独测试服务器应用程序 - 特别是,我想排除所有(或至少大部分)对外部服务的 http 调用。这应该使测试运行得更快,更不容易受到外部故障(服务不可用等)的影响,并使我们能够更好地控制超时或 5xx 错误等测试场景(更不用说允许我们针对仍在设计中的 API 进行开发/发展)。
这样做的一个主要障碍是找到一种很好的、健壮的方式来生成和维护设备。理想情况下,我想对外部依赖项进行真正的调用,记录请求和响应,然后保留存根,以便它们可以用于标准的、非代理运行的测试。理想情况下,我希望这两个操作使用相同的测试代码,这样我就不必维护一个单独的测试套件来负责维护设备(这将主要复制实际测试的代码)。此外,在第一次生成存根后,我可能想对它们进行一些手动修改(例如,修复一些匹配器、在请求中将值注入响应等)。
最后一部分,这是真正棘手的部分:对于存根维护,我希望能够按需在代理模式下运行测试,并且 如果现有存根以有意义的方式不同,则更新它们来自新录制的存根。 “有意义的方式”意味着消息的内容/结构应该保持不变(例如,请求中的时间戳字段,我希望每次调用都不同)。
我已经能够使用 WireMock 弄清楚其中的大部分内容,并且查看 MockServer 似乎它支持的内容与 WireMock 一样多,甚至更多。然而,问题出现在最后一部分,似乎他们都没有处理得特别好。下面我概述了我到目前为止所做的事情,以及我在哪些方面遇到了困难。
首先,我有一个简单的 API /files,它有一个我想测试的 POST 端点。端点将请求对象作为输入,其中包含有关文件的一些元数据,并返回一个 JSON 正文,其中包含文件的标识符和用于上传文件的预签名 URL。
作为请求身份验证的一部分,我们调用外部身份验证服务以断言用户(由 cookie 标识)已登录并获取有关用户的一些信息。
因此,代码看起来像这样(使用 JUnit 5、WireMock 和 REST-assured)(在其他地方,我将代码中的身份验证服务器请求重定向到 localhost:9090)。 :
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FilesApiTest {
@LocalServerPort
public int port;
private WireMockServer wireMockServer;
@BeforeEach
public void setup() {
RestAssured.port = port;
wireMockServer = new WireMockServer(9090);
wireMockServer.start();
WireMock.configureFor(9090); // not sure why it doesn't work without this
}
@AfterEach
public void takeDown() {
wireMockServer.stop();
}
@Test
public void test_files_create() throws Exception {
given()
.body(CreateFileRequest.builder()
.fileName("fileName")
.fileSize(1000L)
.build())
.cookie("authid", "648e5a0d-4fd8-4465-9a76-90d3f4c85142")
.contentType(ContentType.JSON)
.when()
.post(FilesController.PATH)
.then()
.body("fileGuid", hasLength(22), "uploadFileLocation", containsString("https://"));
}
}
好的开始,但是如果没有映射文件/stubFor 调用,身份验证请求就会失败,因为我们实际上并没有存根任何东西。因此,我通过将所有请求代理到外部 API 并记录/保留映射来生成一些存根文件。
@Test
public void test_files_create() throws Exception {
wireMockServer.startRecording("http://otherservice.com");
given()
... // omitted for brevity
wireMockServer.stopRecording();
}
现在我的 resources/mappings
文件夹中有一个映射文件。它看起来像这样:
{
"id": "6a9e5e07-74b7-46d8-9c52-822e6b261518",
"insertionIndex": 1,
"name": "login",
"persistent": true,
"request": {
"url": "/login",
"method": "POST",
"bodyPatterns": [{
"equalToJson" : "{\"authid\":\"648e5a0d-4fd8-4465-9a76-90d3f4c85142\"}",
"ignoreArrayOrder" : true,
"ignoreExtraElements" : true
}]
},
"response": {
"status": 200,
"body": "{\"Uuid\":\"04a81164-91d7-4552-9afe-c7c15c1b7d0b\",\"authid\":\"648e5a0d-4fd8-4465-9a76-90d3f4c85142\"}",
"headers": {
"Date": "Mon, 19 Jul 2021 18:58:59 GMT",
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Strict-Transport-Security": "max-age=16000000; includeSubDomains; preload;",
"X-Frame-Options": "SAMEORIGIN"
}
},
"uuid": "0e0874c4-3ebd-435b-824c-0a7113664fdc"
}
我修改了请求正文模式,因为传入的 cookie 是灵活的:
"bodyPatterns": [
{
"matchesJsonPath": "$.authid"
}
]
并且我在请求 authid 中将响应正文修改为模板,因为它始终只输出输入的内容:
"body": "{\"Uuid\":\"04a81164-91d7-4552-9afe-c7c15c1b7d0b\",\"authid\":\"{{jsonPath request.body '$.authid'}}\"}",
现在映射足够灵活,可以处理它应该处理的所有请求。此时,我有两个问题需要解决:
第一个可能可以通过几种方式解决。我采用了一种简单的方法,即注入布尔属性(由属性文件的 env 变量提供)并在启用代理的情况下运行记录。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FilesApiTest {
@Value("${wiremock.proxying}")
public boolean proxyingEnabled;
// omitted
@Test
public void test_files_create() throws Exception {
if (proxyingEnabled) {
wireMockServer.startRecording("http://otherservice.com")
}
given()
// omitted
if (proxyingEnabled) {
wireMockServer.stopRecording();
}
}
}
非常基本的修复,但目前适用于我的用例。
第二个问题很难解决。到目前为止,我的一般方法是默认禁用持久映射,在记录停止后拉出现有映射,然后进行比较。如果发现差异,请使用 wireMockServer.editStubMapping()
用差异更新现有存根映射。但这感觉非常复杂,并且手挥动了实际困难的部分 - 做某种轻松的等于比较。目前,如果我直接比较这两个映射,会有一些差异:
$.uuid
不同(由 WireMock 随机生成)$.id
不同(由 WireMock 随机生成)$.insertionIndex
是不同的(不太确定这有什么用)$.response.headers.Date
不同(请求的当前日期时间)$.request.bodyPatterns[0]
不同(因为它是手动修改的)$.response.body.authid
是不同的(因为抓取现有的映射不会注入任何东西,所以它是 jsonPath 注入器文字与实际 id 的比较。我想理想地将主体与所做的所有替换进行比较)立>
这是相当多的“预期差异”,其中一些是针对此请求的(尤其是 5 和 6)。所以我留下了如何在两个中等复杂对象之间进行松散比较的问题。不仅如此,我还需要制定一个足够健壮和干净的解决方案,以便我可以轻松地为每个 API 调用实现它。这可能是可行的,但感觉非常令人生畏,而且相当笨拙。特别是因为我的服务调用了一堆不同的服务,每个服务的比较可能略有不同。
我想相信有一种更简单的方法可以做到这一点,或者我在某个时候误入歧途,我完全错误地处理了这个问题。在这一点上,我真的很愿意接受任何形式的建议,因为我正在认真考虑,如果这会让人头疼的话,就放弃剔除外部依赖项。