[Unit Test][Rhino.Mocks]讓 Stub 物件同一方法被呼叫兩次,回傳不同值

  • 4671
  • 0

[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 只做幾件事:

  1. 檢查 request 簽章是否合法:包含了從 request 中讀出簽章的內容,接著從 request 中找到相關的資訊,進行 hash 運算,取得自行運算後的簽章結果。把 client 端傳過來的簽章,與 server 自己用 hash 運算後的簽章進行比較。
  2. 若不相等,代表簽章驗證失敗,則回傳一個 Unauthorized 的 response。
  3. 若相等,代表簽章驗證成功,呼叫 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/