如今,完全獨立的業務應用幾乎不存在,不管是在企業內部微服務之間的呼叫,還是與外部第三方服務的呼叫,Http的API互動是常見的場景,這些實際情況給我們的開發帶來了比較大的挑戰,一是第三方服務可能會牽制我們的開發進度,特別是在多團隊開發的情況下,由於依賴於其他團隊的服務,有時候需要等待其他團隊的進度,導致自己團隊的無效等待。有時因為其他團隊的延期,導致團隊的被動延期。二是第三方服務的質量問題或開發過程中的頻繁更新導致的部署問題,將嚴重拖累自己團隊的開發進度,同時讓你無法專心的開發自己的服務。三是單元測試困難,特別是在依賴於多個第三方服務時,使得單元測試可能依賴於其他服務環境,導致單元測試結果的不確定性。
為了解決以上這些問題,Xfrogcn.AspNetCore.Extensions擴充套件庫提供了Http請求模擬的功能,通過此功能可以讓你在開發、單元測試時實現你的服務與第三方服務的完全解耦,讓你能夠更聚焦於自己服務的開發。
Http請求模擬構建在.NET Core HttpClientFactory架構之上,通過在HttpClient請求管道中替換實際傳送Http請求的主訊息處理器為模擬訊息處理器來完成請求的模擬應答。
一、在服務端使用
假設我們負責開發一個訂單服務,在訂單提交介面,我們儲存完訂單資料之後,需要傳送訊息通知,訊息通知的傳送由訊息服務來實現,該服務由另一團隊負責,如下圖所示:
由於訂單服務依賴於訊息服務,在專案啟動時,一般兩個團隊會協商好訊息服務的介面定義,然後訊息服務團隊會快速搭建一個空介面供訂單服務團隊呼叫,如果是這種流程,訂單服務團隊只需等待訊息服務團隊搭建好環境即可開始工作,好像影響不大,但在實際開發過程中,會存在以下現實的問題:
- 雖然訊息服務團隊提供空介面的時間不長,但是如果專案工期緊張,計劃都是以小時計算,那麼這也將影響訂單服務的開發進度
- “空訊息服務”實際上無法一直保持空的狀態,訊息服務團隊會不斷對服務進行更新加入他們的實現邏輯,而訊息服務本身也可能依賴於其他的服務,這導致訂單團隊所使用的訊息服務不穩定,那麼訂單團隊的進度實際上還是會受到訊息服務團隊,以及訊息服務所依賴的其他團隊的影響。
- 訂單服務團隊可以使用空的訊息服務,但訊息服務團隊往往需要連線企業外部的第三方服務,比如App的訊息推送通道,這讓整個專案依賴更加複雜。
- 訂單服務團隊編寫單元測試會比較困難(當然,此點可以通過抽象來解決,但結合擴充套件庫的Http請求模擬功能,我們可以簡化此過程)
以下介紹如何使用擴充套件庫的請求模擬功能。
為了聚焦於模擬功能的演示,該示例進行了簡化,比如與訊息服務的通訊,在正式專案中會通過訊息服務的SDK來完成,示例中將直接使用HttpClient,有關SDK與擴充套件庫的結合,我們將在後續文章中說明。
- 引用Xfrogcn.AspNetCore.Extensions
- 定義訂單類
public class Order
{
public string Id { get; set; }
public string ProductCode { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
- 定義訊息傳送請求類
public class SendMessageRequest
{
public string MessageId { get; set; }
public string Message { get; set; }
public List<int> UserIds { get; set; }
}
- 配置
在Starup ConfigureServices方法中配置模擬
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 啟用擴充套件庫
services.AddExtensions(Configuration);
// 訊息服務所使用的HttpClient名稱MESSAGESERVICE
IHttpClientBuilder messageClient = services.AddHttpClient("MESSAGESERVICE", client =>
{
// 設定基礎地址
client.BaseAddress = new Uri("http://api.hello.com/");
});
// 只有Mock配置設定為true時,才啟用,通過開發應用配置檔案來配置
if (Configuration.GetValue<bool>("Mock"))
{
// 配置針對訊息服務客戶端,POST到/message/send介面的請求,都將返回一個ResponseMessage
messageClient.AddMockHttpMessageHandler()
.AddMock<ResponseMessage>("/message/send", HttpMethod.Post, new ResponseMessage());
}
}
注意,以上通過配置中的Mock屬性來決定是否開啟模擬功能,為了不影響正式釋出,可以通過開發環境配置(appsettings.Development.json)來開啟模擬:
{
"Mock": true
}
- 控制器
[Route("api/order")]
[ApiController]
public class OrderController : ControllerBase
{
readonly HttpClient messageClient;
public OrderController(IHttpClientFactory clientFactory)
{
// 建立訊息服務所使用的客戶端,名稱與配置所使用的名稱一致
// 實際專案中千萬不要寫上哦~
messageClient = clientFactory.CreateClient("MESSAGESERVICE");
}
[HttpPost]
public async Task<ResponseMessage> SaveOrder([FromBody]Order order)
{
// 儲存訂單,省略了....
// 呼叫訊息服務介面
ResponseMessage response = await messageClient.PostAsync<ResponseMessage>(
"/message/send", new SendMessageRequest()
{
MessageId = Guid.NewGuid().ToString("N").ToLower(),
Message = "訂單已提交",
UserIds = new List<int>() { 1,2,3}
});
// 根據訊息服務返回應答繼續處理,省略了...
return response;
}
}
- 啟動,然後通過Api測試工具(如Postman)向/api/order POST請求,介面將返回以下應答:
{
"code": "0",
"message": null,
"isSuccess": true
}
如上,通過Http請求模擬,我們實現了訂單服務對訊息服務的依賴。
二、在單元測試中使用
單元測試中,針對模擬應答的配置是一樣的,我們可以通過測試用例模擬各種不同的應答,包括異常,來對執行路徑進行測試。
[Fact]
public async Task Test1()
{
IServiceCollection services = new ServiceCollection()
.AddExtensions();
services.AddHttpClient("TESTCLIENT")
.AddMockHttpMessageHandler()
// 請求/test/exception將觸發異常
.AddMock("/test/exception", HttpMethod.Get, new Exception(""))
// 針對 /test/404 返回404應答
.AddMock("/test/404", HttpMethod.Get, HttpStatusCode.NotFound)
// 返回指定型別
.AddMock<int>("/test/obj", HttpMethod.Get, 100)
// 自定義條件及應答
.AddMock(request =>
{
if (request.Headers.Contains("hello"))
{
return true;
}
return false;
}, async (request, response) =>
{
// 如果是呼叫第三方服務,你可以在這裡檢查request發出的請求內容是否正確
// 自定義應答內容
await response.WriteObjectAsync(new
{
test = "Hello World"
});
})
// 針對所有請求返回字串Hello
.AddMock("*", HttpMethod.Get, "Hello");
IServiceProvider provider = services.BuildServiceProvider();
IHttpClientFactory clientFactory = provider.GetRequiredService<IHttpClientFactory>();
HttpClient client = clientFactory.CreateClient("TESTCLIENT");
client.BaseAddress = new Uri("http://localhost");
HttpResponseMessage resposne = await client.GetAsync("/test/404");
Assert.Equal(HttpStatusCode.NotFound, resposne.StatusCode);
}
三、示例
詳細示例請參考GitHub