[Unit Test][Rhino.Mocks]讓 Stub 物件同一方法被呼叫兩次,回傳不同值
前言
在使用 Rhino.Mocks 來動態產生 stub 物件,模擬外部相依物件回傳值時,相當方便。通常我們都不關心 input 的 parameter 為何,只關心 return 的值,所以 parameter 都會直接使用 Arg<T>.Is.Anything
。
不過上次同事碰到一個需求是,呼叫同一個方法兩次,第一次跟第二次希望 stub 回傳的結果不一樣,且希望參數仍是使用 Arg<T>.Is.Anything
。
這一篇文章就簡單帶一下,該怎麼滿足這樣的需求。
範例 – SignatureMessageHandler
還不知道如何針對 Messge Handler 進行單元測試的朋友,可以參考一下前一篇文章:[Web API]如何針對 Message Handler 進行單元測試
這邊舉的例子是用來「驗證跟附加簽章的 message handler 」,程式碼如下:
public interface IHashService
{
string GetSignature(string content);
}
public class SignatureMessageHandler : DelegatingHandler
{
private const string SignatureHeaderName = "api-signature";
private IHashService _hashService;
public SignatureMessageHandler(IHashService hashService)
{
this._hashService = hashService;
}
protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var isValid = this.CheckSignature(request);
if (isValid)
{
return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>(
task =>
{
return this.AppendSignature(task.Result);
});
}
else
{
var taskCompletionSource = new TaskCompletionSource<HttpResponseMessage>();
var response = request.CreateResponse(HttpStatusCode.Unauthorized);
taskCompletionSource.SetResult(response);
var invalidResult = taskCompletionSource.Task;
return invalidResult;
}
}
private HttpResponseMessage AppendSignature(HttpResponseMessage httpResponseMessage)
{
var contentForSignature = this.GetContent(httpResponseMessage);
var signatureFromHashService = this._hashService.GetSignature(contentForSignature);
httpResponseMessage.Headers.Add(SignatureHeaderName, signatureFromHashService);
return httpResponseMessage;
}
private bool CheckSignature(HttpRequestMessage request)
{
var contentForSignature = this.GetContent(request);
var signatureFromHashService = this._hashService.GetSignature(contentForSignature);
var signatureFromReuqestHeader = Enumerable.Empty<string>();
var isExistSignatureHeader = request.Headers.TryGetValues(SignatureHeaderName, out signatureFromReuqestHeader);
if (!isExistSignatureHeader)
{
return false;
}
else
{
return signatureFromHashService == signatureFromReuqestHeader.LastOrDefault();
}
}
private string GetContent(HttpResponseMessage httpResponseMessage)
{
// 實作你要組合供signature用的內容, 例如 內容, saltkey
return httpResponseMessage.Content == null ? string.Empty : httpResponseMessage.Content.ReadAsStringAsync().Result;
}
private string GetContent(HttpRequestMessage request)
{
// 實作你要組合供signature用的內容,例如時戳, token, 內容, saltkey
return request.Content == null ? string.Empty : request.Content.ReadAsStringAsync().Result;
}
}
這個 message handler 只做幾件事:
- 檢查 request 簽章是否合法:包含了從 request 中讀出簽章的內容,接著從 request 中找到相關的資訊,進行 hash 運算,取得自行運算後的簽章結果。把 client 端傳過來的簽章,與 server 自己用 hash 運算後的簽章進行比較。
- 若不相等,代表簽章驗證失敗,則回傳一個 Unauthorized 的 response。
- 若相等,代表簽章驗證成功,呼叫 base.SendAsync() 將 request 繼續往後送。當 request 處理完畢後,會回傳一個 Task<HttpResponseMessage> , 這時 SignatureMessageHandler 拿到這個 response 時,要再將 response 的簽章 append 到 header 上,供 client 端驗證簽章。
GetContent() 方法就是每個 ap 自己實作組合簽章 hash 前的內容,通常 request 會有時戳、token、內容,然後雙方會有 saltkey。上面例子只是為了解說方便,因此直接拿 requset 與 response 的 Content 來做簽章。
這邊要留意的是,this._hashService.GetSignature() 被呼叫了兩次,第一次是用來驗證 request 的簽章是否合法,第二次則是用來附加 response 的簽章。等等的測試程式就會針對這個地方來設計 stub 物件的行為。
單元測試
在初始化 SignatureMessageHandler 時,就要決定 IHashService 的行為了,因此透過 Stub 來進行,程式碼如下:
[TestMethod]
public void 簽章驗證失敗_應回傳Unauthorized()
{
// arrange
var stubHash = MockRepository.GenerateStub<IHashService>();
// 透過.Repeat.Once() 來控制 stub 這個行為只會被觸發一次
stubHash.Stub(x => x.GetSignature(Arg<string>.Is.Anything)).Return("request content signature").Repeat.Once();
stubHash.Stub(x => x.GetSignature(Arg<string>.Is.Anything)).Return("response content signature").Repeat.Once();
var target = new SignatureMessageHandler(stubHash);
//todo
//act
//assert
}
這邊在 stub.Return() 之後,我們加上了 Repeat.Once() , 如字面上的意義,代表這個行為只會有效一次。因此連寫兩行的意思,就代表被呼叫第一次時,IHashService 會回傳 "request content signature" ,而第二次會回傳 "response content signature" 。
是的,就是這麼簡單!接下來把所有測試程式完成後,程式碼如下:
[TestMethod]
public void 簽章驗證失敗_應回傳Unauthorized()
{
// arrange
var stubHash = MockRepository.GenerateStub<IHashService>();
// 透過.Repeat.Once() 來控制 stub 這個行為只會被觸發一次
stubHash.Stub(x => x.GetSignature(Arg<string>.Is.Anything)).Return("request content signature").Repeat.Once();
stubHash.Stub(x => x.GetSignature(Arg<string>.Is.Anything)).Return("response content signature").Repeat.Once();
var target = new SignatureMessageHandler(stubHash);
target.InnerHandler = new FakeInnerHandler
{
Message = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("fake response")
}
};
var client = new HttpMessageInvoker(target);
var request = new HttpRequestMessage(HttpMethod.Post, "http://mytest/api/fake");
request.Headers.Add("api-signature", "different signature");
// act
var actual = client.SendAsync(request, new CancellationToken()).Result;
// assert
Assert.AreEqual(System.Net.HttpStatusCode.Unauthorized, actual.StatusCode);
Assert.IsNull(actual.Content);
}
[TestMethod]
public void 簽章驗證成功_應回傳OK()
{
var stubHash = MockRepository.GenerateStub<IHashService>();
// 透過.Repeat.Once() 來控制 stub 這個行為只會被觸發一次
stubHash.Stub(x => x.GetSignature(Arg<string>.Is.Anything)).Return("request content signature").Repeat.Once();
stubHash.Stub(x => x.GetSignature(Arg<string>.Is.Anything)).Return("response content signature").Repeat.Once();
var target = new SignatureMessageHandler(stubHash);
target.InnerHandler = new FakeInnerHandler
{
Message = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("fake response")
}
};
var client = new HttpMessageInvoker(target);
var request = new HttpRequestMessage(HttpMethod.Post, "http://mytest/api/fake");
request.Headers.Add("api-signature", "request content signature");
// act
var actual = client.SendAsync(request, new CancellationToken()).Result;
// assert
Assert.AreEqual(System.Net.HttpStatusCode.OK, actual.StatusCode);
Assert.AreEqual("fake response", actual.Content.ReadAsStringAsync().Result);
var responseSignatureHeader = Enumerable.Empty<string>();
Assert.IsTrue(actual.Headers.TryGetValues("api-signature", out responseSignatureHeader));
Assert.AreEqual("response content signature", responseSignatureHeader.LastOrDefault());
}
因為這裡 stubHash 並非測試案例所要驗證的行為,因此只需要透過 stub object, 而不需要透過 mock object 來驗證與 IHashService 的互動, stub 與 mock 的差異,建議可以參考之前的文章:[30天快速上手TDD][Day 7]Unit Test - Stub, Mock, Fake 簡介 與 Microsoft Fakes 入門
結論
雖然只是簡單的 Repeat.Once() 的簡介,不過還是希望可以幫助到有類需求的朋友,也可以再回味一下, message handler 有多有趣!
blog 與課程更新內容,請前往新站位置:http://tdd.best/