Problem
我有一隻待測程式(SUT)會相依於外部服務(External Service),操作流程如下:
- SUT觸發一個async command。
- 接著外部服務將會重置而導致SUT無法連線。
- SUT會不停的Get去等待外部服務再次連線。
Network failure trying to make a proxied request from WireMock to https://10.146.125.169/api/v1 Connect to 10.146.125.169:443 [/10.146.125.169] failed: Connection timed out: connect
這導致SUT接到非預期的500 status code,使工作的執行無法順利完成。本篇文章主要分享我的解決方法。(code: link)
How to?
(對嘗試過程沒興趣可以直接看方法2)
方法1
我的第一個方法,是直接找到造成500問題的地方(link ProxyResponseRenderer),修改WireMock程式碼直接retry。這會讓工作能夠繼續執行並完成。最後我透過修改錄製出來的腳本,加上長時間的response delay強迫client timeout去模擬類似情境:
"response": { "status": 200, "fixedDelayMilliseconds": 120000 }
Note. 嘗試過在HttpClientFactory加socket timeout是沒有幫助的,這受到作業系統設定限制。
方法2
後來研究了一下Extending WireMock內容後,覺得應該有機會可以透過擴充的方式去達到我的目的,因此有了方法2。
Extend ResponseTransformer
首先我看上了ResponseTransformer,它可以讓你對Response加工。在Simulating Faults內容有提及WireMock模擬Fault的一些方式,其中這部分內容激起了我的靈感:
{ "request": { "method": "GET", "url": "/fault" }, "response": { "fault": "MALFORMED_RESPONSE_CHUNK" } }
我想應該可以根據connection timed out這種特定錯誤情境,讓WireMock產生Fault的Response,在這裡我使用了Fault.CONNECTION_RESET_BY_PEER:
public class SocketTimedOutResponseTransformer extends ResponseTransformer { @Override public Response transform(Request request, Response response, FileSource files, Parameters parameters) { if(response.getStatus()==500&&response.getBodyAsString().contains("Network failure")) { return Response.Builder.like(response) .body(String.valueOf(Fault.CONNECTION_RESET_BY_PEER)) .fault(Fault.CONNECTION_RESET_BY_PEER).build(); } return response; } @Override public String getName() { return "SocketTimedOutResponseTransformer"; } }
response我除了模擬fault以外,也在body塞Fault.CONNECTION_RESET_BY_PEER;這是另一段故事,請讓我在下一段做說明。
Extend StubMappingTransformer
在套用上面方法後,工作是能夠如下圖執行完畢:
但產生出來的腳本並不包含fault動作,也代表著無法重現整個流程。我Trace了一下WireMock程式碼,發現LoggedResponse轉為ResponseDefinition時,並沒有取用Fault欄位,這導致了資料的遺失:
因此我將腦筋動到了另外一個Extension StubMappingTransformer上。與前一個extension成對,當收到500且有Fault的ResponseDefinition時,就會把body內的字串轉為Fault並產生新的ResponseDefinition,最後塞到StubMapping讓它有辦法產生我們預期的內容:
public class FaultStubMappingTransformer extends StubMappingTransformer { @Override public String getName() { return "FaultStubMappingTransformer"; } private Fault getFault(ResponseDefinition responseDef) { String body = Objects.toString(new String(responseDef.getByteBody()), ""); try { return Fault.valueOf(body); } catch (IllegalArgumentException e) { return null; } } private void setupFaultResponseDef(StubMapping stubMapping) { ResponseDefinition responseDef = stubMapping.getResponse(); Fault fault = getFault(responseDef); if( fault == null ) return; ResponseDefinition faultResponseDef = ResponseDefinitionBuilder.like(responseDef).withFault(fault).build(); stubMapping.setResponse(faultResponseDef); } @Override public StubMapping transform(StubMapping stubMapping, FileSource files, Parameters parameters) { ResponseDefinition responseDef = stubMapping.getResponse(); if( responseDef.getStatus() == 500) { setupFaultResponseDef(stubMapping); } return stubMapping; } }
最後產生的mapping file終於有fault的字眼:
"response" : { "status" : 500, "base64Body" : "Q09OTkVDVElPTl9SRVNFVF9CWV9QRUVS", "fault" : "CONNECTION_RESET_BY_PEER" }
留言
張貼留言