Rhino Mocks 3.5

本文介紹了一些新的概念,大部分是基於C#3.0語言特徵。可是C#2.0的用戶沒必要擔憂,由於有着大量吸引他們的東西(例如內聯約束)。可是本文的關注點在於如何在C#3.0中使用Rhino Mocks。html

Rhino Mocks的新增內容

Rhino Mocks 3.5中的新事物有:正則表達式

  • AAA模式,即Arrange(數據準備、執行行爲或方法、斷言狀態或條件)
  • Lambda和C#3.0擴展
  • 屬性Setter顯式預期API
  • 支持C++中的接口模擬,混合了本地和託管類型
  • 容許一個模擬對象返回記錄模式而不失其預期
  • CreateMock被棄用,推薦使用StrictMock
  • 在邊界案例中有更好的錯誤處理
  • 修復了模擬內部類和接口時的一個問題
  • 新的事件喚起語法

使用指南

一般,我建議遵循「Test only one per test」(每一個測試只測試同樣,測試點單一)。將此應用到Rhino Mocks,每一個單元測試應該只驗證與另外一個對象不超過1個的顯著交互。這就是說,一個給定的測試,不該該有多個模擬對象,可是根據需求,可能會有多個樁。(一個例外的狀況是,一個測試自己就須要兩個以來對象的預期,例如一個對象的方法在另一個對象的方法執行後才能被執行)。ide

樁與模擬對象的不一樣

你能夠從 Mocks Aren't Stubs這篇文章中獲取這些術語的實際定義。本文只從Rhino Mocks的視角關注兩者的不一樣。單元測試

一個模擬(mock)是一個咱們可以設置預期的對象,並驗證預期的行爲真實發生了(模擬須要預先編寫好預期,造成一個指望接收到的規範調用)。測試

一個樁(stub)是一個用於傳遞給被測試的代碼的對象,你能夠對它設置預期,它將按某種方式執行,但這些預期將不會被驗證。樁的屬性與普通屬性行爲表現同樣,並且不能對它們設置預期(樁是針對調用提供錄製的答案,不會響應測試外的東西)。this

若是想要驗證被測試代碼的行爲,就應該使用設置了合適的預期的模擬對象,並驗證這些預期。若是你只想要傳遞一個值, 它可能會按照某種方式行事,但它不是測試的重點,就應該使用樁。spa

重要提示:樁永遠不會致使測試失敗!翻譯

下面讓咱們建立一個類,它將帶給咱們更多的討論。咱們想要編寫一個對忘記密碼時發送SMS的測試。代碼應該是這樣的:3d

  • 從存儲庫獲取用戶
  • 將密碼重置到一個隨機值
  • 保存更改密碼後的用戶
  • 使用新密碼發送SMS

首先,咱們想要測試密碼重置,因此測試代碼是這樣的:code

[Test]
        public void Test_When_User_Forgot_Password_Should_Reset_Password()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();
            var stubedSmsSender = MockRepository.GenerateStub<ISmsSender>();

            var theUser = new User() {HashedPassword="this is not hashed password"};

            var loginController = new LoginController(stubUserRepository, stubedSmsSender);

            loginController.ForgotMyPassword("ayende");

            Assert.AreNotEqual("this is not hashed password",theUser.HashedPassword);
        }
Test_When_User_Forgot_Password_Should_Reset_Password

 

在這個測試中,咱們建立了兩個樁,並將他們傳給被測試的代碼。爲了使得測試經過,咱們能夠這樣:

public void ForgotMyPassword(string username)
        {
            var user = _userRepository.GetUserByName(username);

            user.HashedPassword = "new pass";
        }
ForgotMyPassword

如今,咱們想要驗證用戶將會保存密碼, 因而有了下面的測試:

[Test]
        public void Test_When_User_Forgot_Password_Should_Save_User()
        {
            var mockUserRepository = MockRepository.GenerateMock<IUserRepository>();
            var mockSmsSender = MockRepository.GenerateMock<ISmsSender>();

            var theUser = new User() { HashedPassword = "this is not hashed password" };

            //使用存根傳遞被測試對象,並設置預期,預期會按某種方式執行,但不會被覈實。此處使用的是存根方法,預期調用GetUserByName時返回TheUser
            mockUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser);

            //使用模擬對象設置預期,預期模擬對象會執行SaveUser的操做
            mockUserRepository.Expect(repository => repository.SaveUser(theUser));

            var loginController = new LoginController(mockUserRepository, mockSmsSender);

            loginController.ForgotMyPassword("ayende");

            //模擬對象覈實預期行爲
            mockUserRepository.VerifyAllExpectations();
        }
Test_When_User_Forgot_Password_Should_Save_User

咱們如今已經有了一個模擬對象,並對它設置了預期。注意,儘管咱們在這裏使用的是模擬對象,咱們仍然能夠經過stub調用GetUserByName。咱們不須要關注着點,由於這並非咱們測試的重點。

如今,被測試的代碼應該是這樣的:

public void ForgotMyPassword(string username)
        {
            var user = _userRepository.GetUserByName(username);

            user.HashedPassword = "new pass";

            _userRepository.SaveUser(user);
        }
ForgotMyPassword

此測試的另一種寫法:

[Test]
        public void Test_When_User_Forgot_Password_Should_Save_User_Replace()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();
            var stubSmsSender = MockRepository.GenerateStub<ISmsSender>();

            var theUser = new User() { HashedPassword = "this is not hashed password" };

            //使用存根傳遞被測試對象,並設置預期,預期會按某種方式執行,但不會被覈實。預期調用GetUserByName時返回TheUser
            stubUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser);

            var loginController = new LoginController(stubUserRepository, stubSmsSender);

            loginController.ForgotMyPassword("ayende");

            //斷言模擬對象會調用指定方法
            stubUserRepository.AssertWasCalled(repository => repository.SaveUser(theUser));
        }
Test_When_User_Forgot_Password_Should_Save_User_Replace

在上面的測試中,咱們沒有預期,但設置了需求狀態,並斷言測試運行時,代碼會執行該操做。兩個測試的不一樣之處很微妙的,但很重要!在大部分的案例中,應該優先使用樁(stub)。只有在測試複雜的交互時,推薦使用模擬對象。若是你想要確保順序,或者期待獲取一個複雜的方法,或者想要確保確切遵照預期,應該使用模擬對象。

到如今爲止,咱們尚未關心過實際發送一個SMS,如今,咱們來編寫一個測試,確保咱們能發送SMS:

[Test]
        public void Test_When_User_Forgot_Password_Send_SMS()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();
            var stubSmsSender = MockRepository.GenerateStub<ISmsSender>();

            var theUser = new User() {HashedPassword = "this is not hashed password", Phone = "1234-1234"};

            stubUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser);

            var loginController = new LoginController(stubUserRepository, stubSmsSender);
            loginController.ForgotMyPassword("ayende");

            stubSmsSender.AssertWasCalled(
                sender => sender.Send(Arg.Is("1234-1234"), Arg.Text.StartsWith("Password was changed to:")));
        }
Test_When_User_Forgot_Password_Send_SMS

在這個測試中,咱們斷言方法會被執行,並使用內聯約束支持來確保傳遞了正確的參數。被測試的代碼(ForgotMyPassword)如今應該是這樣的:

public void ForgotMyPassword(string username)
        {
            var user = _userRepository.GetUserByName(username);

            user.HashedPassword = "new pass";

            _userRepository.SaveUser(user);

            _sender.Send(user.Phone, "Password was changed to:" + user.HashedPassword);
        }
ForgotMyPassword

記住:咱們不但願被測試束縛住。這點很重要。咱們但願確保的是:儘管咱們修改了被測試的代碼時,仍然不想破壞其餘無關的測試。

CreateMock方法被棄用,被替換爲StrictMock。不鼓勵使用精確模擬(Strict Mock)。

在Rhino Mocks3.4及以前的版本中,獲取模擬對象的默認方式是調用mocks.CreateMock(ISmsSender)。這種方式形成了很是嚴重的問題。CreateMock()方法將返回一個精確模擬(Strict Mock),並且就像咱們已經討論過的,過於嚴格的模擬將使得測試很脆弱。因爲這點,CreateMock()被棄用,而且若是被使用將會在生成一個編譯警告。你能夠用StrictMock()方法替代CreateMock()方法。

然而,不鼓勵使用StrictMock()。若是某些東西在它們身上未能按預期那樣發生,精確模擬(Strict Mock)將會失敗。從長遠來看,這意味着任何對被測試代碼的修改都將致使你的測試失敗,儘管這些改變與你實際正在進行的特定測試無關。

所以,我鼓勵使用樁(stubs)和動態模擬對象(dynamic mock)。

使用MockRepository模擬和不使用MockRepository模擬

Rhino Mock3.5引入了一種新的模擬方式,在不需建立存儲庫(Repository)的狀況下使用模擬。The purpose of that is to reduce the amount of book keeping that you have to do(啥玩意兒!徹底看不懂)。

這是不使用存儲庫時的測試:

[Test]
        public void Test_When_User_Forgot_Password_Should_Save_User()
        {
            var mockUserRepository = MockRepository.GenerateMock<IUserRepository>();
            var stubedSmsSender = MockRepository.GenerateStub<ISmsSender>();

            var theUser = new User() {HashedPassword = "this is not hashed password"};

            //使用存根傳遞被測試對象,並設置預期,預期會按某種方式執行,但不會被覈實。此處使用的是存根方法,預期調用GetUserByName時返回TheUser
            mockUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser);

            //使用模擬對象設置預期,預期模擬對象會執行SaveUser的操做
            mockUserRepository.Expect(repository => repository.SaveUser(theUser));

            var loginController = new LoginController(mockUserRepository, stubedSmsSender);

            loginController.ForgotMyPassword("ayende");

            //模擬對象覈實預期行爲
            mockUserRepository.VerifyAllExpectations();
        }
Test_When_User_Forgot_Password_Should_Save_User

使用存儲庫時,測試是這樣的:

[Test]
        public void Test_When_User_Forget_Password_Should_SaveUser_With_Repository()
        {
            var mocks = new MockRepository();
            var mockUserRepository = mocks.DynamicMock<IUserRepository>();
            var stubedSmsSender = mocks.Stub<ISmsSender>();

            using (mocks.Record())
            {
                var theUser = new User() {HashedPassword = "this is not hashed password"};
                mockUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser);
                mockUserRepository.Expect(repository => repository.SaveUser(theUser));
            }

            using (mocks.Playback())
            {
                var loginController = new LoginController(mockUserRepository, stubedSmsSender);
                loginController.ForgotMyPassword("ayende");
            }
        }
Test_When_User_Forget_Password_Should_SaveUser_With_Repository

這兩個方法有幾點不一樣:

  • MockRepository.GenerateMock()和MockRepository.GenerateStub()是在replay模式下返回模擬對象和樁對象所以不須要顯式的移動到replay模式
  • 只有動態模擬(Dynamic Mock)和樁(Stub)不建立存儲庫(Repository)。(這樣處理的背景是假設它們是最經常使用的,而且這麼處理時值得的)
  • 若是沒有建立存儲庫(Reponsitory),你須要針對每一個被建立的模擬對象調用VerifyAllExpectations()方法。若是建立了存儲庫,只需調用存儲庫的VerifyAll()方法便可,它會爲你處理全部的工做。因爲在大部分的環境中,每一個測試只須要一個模擬對象(而且樁不須要驗證),這點並非什麼問題。

Arrange,Act,Assert

在Rhino Mock3.5中最重要的功能是AAA語法。Arrange,Act,Assert是一個組織你的測試的經典方式。首先,安排狀態;而後,執行被測試的代碼;最後,斷言你指望的狀態發生改變或行爲觸發了。基於交互的測試,一般是很難進行的,並且還要求使用Record/Play模式。這種案例對基於交互測試的新手來講是很困惑的。AAA語法恰好解決了這個問題。

讓咱們來看一個簡單的測試,並按AAA方式來對它進行分析:

[Test]
        public void Test_When_User_Forget_Password_Should_SaveUser_With_AAA()
        {
            //Arrange
            var stubedUserRepository = MockRepository.GenerateStub<IUserRepository>();
            var stubedSmsSender = MockRepository.GenerateStub<ISmsSender>();

            var theUser = new User() {HashedPassword = "this is not hashed password"};

            stubedUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser);

            //Act
            var loginController = new LoginController(stubedUserRepository, stubedSmsSender);

            loginController.ForgotMyPassword("ayende");

            //Assert
            stubedUserRepository.AssertWasCalled(repository => repository.SaveUser(theUser));
        }
Test_When_User_Forget_Password_Should_SaveUser_With_AAA

咱們能夠很容易的看到測試的不一樣階段。這種方法的行爲的主要支持者是Expect()和Stub()方法。

Expect和Stub擴展方法

當你引用了Rhino.Mocks名稱空間後,模擬對象可使用Expect()和Stub()擴展方法。Stub()方法對於樁對象也是可用的。

Expect擴展方法

Expect()方法爲模擬對象建立了一個新的預期,這個預期必須在後面被驗證或斷言(使用VerifyAllExpectations()或AssertWasCalled)。

Expect功能相似於以前版本的Rhino Mocks的Expect.Call()功能,Expect()擴展方法可以在沒有Record()塊時使用(Expect.Call()必須在顯式的Record()塊中使用)。

考慮下面的例子的用法,它確保了控制器的SingOut操做調用了mockAuthSvr.SignOut()方法:

[Test]
        public void Test_Should_SignOut_of_AuthSerive_When_The_LoginOut_Is_Invoked()
        {
            var mockAuthSvr = MockRepository.GenerateMock<IAuthService>();

            mockAuthSvr.Expect(service => service.SignOut()).Return(true);

            var signOutController = new SignOutController(mockAuthSvr);
            signOutController.SignOut();

            mockAuthSvr.VerifyAllExpectations();
        }
Test_Should_SignOut_of_AuthSerive_When_The_LoginOut_Is_Invoked

Stub()擴展方法

Stub()擴展方法能夠stub一個指定成員,使其能夠執行默認的樁行爲(例如,使測試不失敗。——對於指定行爲請查看本文檔中以前關於stub的定義)。

若是你想要以某種方式影響代碼,就應該使用這種方式。記住:默認的,Rhino Mocks會忽略爲預期的方法調用。下面即是一好的例子:

public void SignOut()
        {
            if(_authService.SignOut())
                _notificationService.UserIsLoggedOut();
        }
SignOutController.SignOut()

下面是測試代碼,咱們樁調用了SignOut並返回true,而後預期調用UserIsLoggedOut()方法:

[Test]
        public void Test_Should_Notify_When_User_SignOut_Successfully()
        {
            //Assert
            var stubAuthSvr = MockRepository.GenerateStub<IAuthService>();
            var mockNotifySvr = MockRepository.GenerateMock<INotificationService>();

            stubAuthSvr.Stub(service => service.SignOut()).Return(true);

            mockNotifySvr.Expect(service => service.UserIsLoggedOut());

            //Act
            var signOutController = new SignOutController(stubAuthSvr, mockNotifySvr);
            signOutController.SignOut();

            //Assert
            mockNotifySvr.VerifyAllExpectations();
        }
Test_Should_Notify_When_User_SignOut_Successfully

另外的一種方式是不顯式的預期方法調用,而是使用斷言指定的方法會被調用:

[Test]
        public void Test_Should_Notify_When_User_SignOut_Successfully_With_Assert()
        {
            //assert
            var stubAuthSvr = MockRepository.GenerateStub<IAuthService>();
            var mockNotifySvr = MockRepository.GenerateMock<INotificationService>();

            stubAuthSvr.Stub(service => service.SignOut()).Return(true);

            //act
            var signOutController = new SignOutController(stubAuthSvr, mockNotifySvr);
            signOutController.SignOut();

            mockNotifySvr.AssertWasCalled(service => service.UserIsLoggedOut());
        }
Test_Should_Notify_When_User_SignOut_Successfully_With_Assert

使用Expect()設置屬性

expect()能夠用來設置預期並返回屬性的值。

下面的例子來自於一個Model-View-Controller項目,測試了Controller的RunView()方法。View中的一個屬性的getter被模擬,並預期它的調用:

public void RunView_Returns_the_View_List
{
   //Arrange
   List<string> expectedlist = new List<string>();
   expectedlist.Add("Ayende");
   expectedlist.Add("Tom");

   Form Parent = null; //Stub, the parent doesn't matter for this test.
   IGetNamesView mockview = MockRepository.GenerateMock<IGetNamesView>();
   mockview.Expect(view => view.ShowDialog(Parent)).Return(DialogResult.OK);

   mockview.Expect(view => view.Names).Return(expectedlist); //Mock for the property getter

   GetNameController target = new GetNameController(Parent, mockview);
   List<string> actual;

   //Act
   actual = target.RunVeiw();

   //Assert
   mockview.VerifyAllExpectations();
   Assert.AreEqual(expectedlist, actual);
}
RunView_Returns_the_View_List

GenerateStub和GenerateMock的區別

在以前版本的Rhino Mocks中,GenrrateMock()等價於MockRepository的實例調用DynamicMock()。GenerateMock()將生成一個動態模擬對象(相對於靜態Mock和Stub)。

在以前版本的Rhino Mocks中,GenrrateStub()等價於MockRepository的實例調用Stub()。GenerateStub會建立一個樁實例,該實例有着樁行爲(查看本文以前關於樁的定義和樁的解釋語義)。

在C#2.0中使用AAA語法

在C#2.0中使用AAA語法也是可行的,只須要咱們理解如何編譯委託擴展方法便可。下面是以前的測試在C#2.0中的寫法:

Test
public static void Test_Using_Extension_Methods_Using_2_0()
{
    IAuthService authSvc = MockRepository.GenerateStub();
    INotificationService notificationSvc = MockRepository.GenerateMock();

    RhinoMocksExtensions.Expect(notificationSvc , delegate(INotificationService o)
    {
        o.UserIsLoggingOut();
    });
    RhinoMocksExtensions.Stub(authSvc, delegate(IAuthService svc)
    {
        svc.SignOut();
    }).Return(true);

    SignoutController controller = new SignoutController(authSvc, notificationSvc);
    controller.SignOut();

    RhinoMocksExtensions.VerifyAllExpectations(notificationSvc);

}

Test
public void Test_Using_AAA_Using_2_0()
{
    IAuthService authSvc = MockRepository.GenerateStub();
    INotificationService notificationSvc = MockRepository.GenerateMock();

    RhinoMocksExtensions.Stub(authSvc, delegate(IAuthService svc)
    {
        svc.SignOut();
    }).Return(true);

    SignoutController controller = new SignoutController(authSvc, notificationSvc);
    controller.SignOut();

    RhinoMocksExtensions.AssertWasCalled(notificationSvc, delegate(INotificationService x)
    {
        x.UserIsLoggingOut();
    });

}
Test in C#2.0

儘管語法不如C#3.0漂亮,但對於C#2.0,咱們仍是有着全部的功能。

參數約束

參數約束屬於預期調用或樁調用的定義。僅當參數約束匹配時,調用規格纔會生效(這意味着:預期獲得知足,返回值纔會被返回;異常被拋出,Do(指的應該是某個方法)纔會被執行,等等)。若是約束不匹配,它就好像調用了另一個方法。

參數約束還能夠經過指定參數類型來定義方法簽名——這就是爲何它們不能被省略。若是你不指定參數約束,將不能定義預期,由於你不能指定方法簽名。

 簡單約束

 指定約束的最簡單的方式是直接使用值,就像在示例中常常見到的:

mockUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser)

字符串值「ayende」是樁規格的一個約束。若是調用GetUserName時使用其餘未指定的參數,將得不到指定的返回值:

[Test]
        public void Test_Argument_Constraints_With_UnSpecified_Value()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();

            var theUser = new User() {Name = "ayende", HashedPassword = "this is not hashed password"};

            stubUserRepository.Stub(repository => repository.GetUserByName("ayende")).Return(theUser);

            Assert.IsNull(stubUserRepository.GetUserByName("Ted"));
            Assert.AreSame(theUser, stubUserRepository.GetUserByName("ayende"));
        }
Test_Argument_Constraints_With_UnSpecified_Value

你也能夠經過IgnoreArguments()方法來指定任何值都是能夠接受的:

[Test]
        public void Test_Argument_Constraints_With_Ignore_Arguments()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();

            var theUser = new User() {Name = "ayende", HashedPassword = "this is not hashed password"};

            stubUserRepository.Stub(repository => repository.GetUserByName(null)).IgnoreArguments().Return(theUser);

            Assert.IsNotNull(stubUserRepository.GetUserByName("Ted"));
            Assert.AreSame(theUser, stubUserRepository.GetUserByName("ayende"));
        }
Test_Argument_Constraints_With_Ignore_Arguments

可是,因爲各類緣由,並不鼓勵使用這種「直接使用一個值」的簡單語法。它很是有限。你僅能定義使用Equals進行比較的約束,而對於引用類型一般是進行引用比較。也很容易出現問題,由於給定的值沒有清晰的指定一個類型。例如上述測試中使用了null做爲實例,它沒有指定類型。若是方法被重寫並使用了不一樣類型的參數,你將不能肯定指定的是哪一個方法(你也能夠指定(string)null實例,但這種方式並不直觀)。

一種可替代的方法是使用內聯約束。

內聯約束

舊版本Rhino Mocks的用戶注意:你可能習慣使用IgnoreArguments(),Constraints()和RefOut()。Rhino Mocks3.5引入了新的約束語法,使用Arg<T>,被稱爲內聯約束。你可使用Arg<T>來替代以前的作法。鼓勵僅使用Arg<T>,儘管寫得更多,但它更連貫,也易於理解。

下面是使用內聯依賴來替代以前的例子:

[Test]
        public void Test_Argument_Constraints_With_Inline_Constraints()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();

            var theUser = new User() {Name = "ayende", HashedPassword = "this is not hashed password"};

            stubUserRepository.Stub(repository => repository.GetUserByName(Arg<string>.Is.Equal("ayende")))
                              .Return(theUser);

            Assert.IsNull(stubUserRepository.GetUserByName("stefan"));
            Assert.AreSame(theUser, stubUserRepository.GetUserByName("ayende"));
        }
Test_Argument_Constraints_With_Inline_Constraints

 

對於首次看到,它可能看起來更長了。你得指定類型,也許你認爲這是沒必要要的。可是請記住,在指定參數類型的同時,還指定了方法簽名。

不只僅限於Equal比較,更多比較引用參見下文。

注意:若是使用了內聯約束,那麼全部的方法參數都應該使用內聯約束來定義。不能與簡單值(plain values)混合使用。

Ignoring Arguments 

[Test]
        public void Test_Argument_Constraints_With_Ignoring_Arguments()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();

            var theUser = new User() {Name = "ayende", HashedPassword = "this is not hashed password"};

            stubUserRepository.Stub(repository => repository.GetUserByName(Arg<string>.Is.Anything))
                              .Return(theUser);

            Assert.IsNotNull(stubUserRepository.GetUserByName("stefan"));
            Assert.AreSame(theUser, stubUserRepository.GetUserByName("ayende"));
        }
Test_Argument_Constraints_With_Ignoring_Arguments

 Equal的快捷方式

Arg.IS<T>(T)實際上Arg<T>.Is.Equal(object)的簡寫,在.NET3.5中,不必指定泛型參數,因而就有了這個更漂亮的簡寫。

stubUserRepository.Stub(repository => repository.GetUserByName(Arg.Is("ayende"))).Return(theUser);

屬性

經過將值分配給屬性,你能夠很輕鬆的指定約束。

[Test]
        public void Test_Property_Constraint()
        {
            var mockUser = MockRepository.GenerateMock<User>();
            //注意User的屬性必須用virtual修飾
            mockUser.Name = "ayende";

            mockUser.AssertWasCalled(user => user.Name = Arg<string>.Is.NotNull);
        }
Test_Property_Constraint

注意:若是你想要預期屬性被讀取,應該在一個匿名委託內將屬性分配到一個變量。

[Test]
        public void Tes_Expect_Property_Read()
        {
            var mockUser = MockRepository.GenerateMock<User>();
            mockUser.Name = "ayende";

            var mockUserName = mockUser.Name;

            //斷言屬性書否被讀取過
            mockUser.AssertWasCalled(user => { var ignor = user.Name; });
        }
Tes_Expect_Property_Read

事件註冊

mock.Load += OnLoad;
mock.AssertWasCalled(x => x.Load += Arg<LoadEvent>.Is.Anything);

複雜表達式

使用Arg<T>.Matches能夠定複雜的表達式。這個方法有兩個重載,一個容許定義一個lambda表達式,另一個容許使用Rhino Mocks約束定義一個複雜的表達式。

[Test]
        public void Test_Complex_Expressions_With_Lambda_Expressions()
        {
            //arrange
            var mockUserRepository = MockRepository.GenerateMock<IUserRepository>();
            var theUser = new User() {Name = "ayende"};

            mockUserRepository.Stub(
                repository =>
                repository.GetUserByName(
                    Arg<string>.Matches(
                        s =>
                        s.StartsWith("aye", StringComparison.InvariantCulture) ||
                        s.StartsWith("stein", StringComparison.InvariantCulture)))).Return(theUser);

            //act and assert
            Assert.AreSame(theUser, mockUserRepository.GetUserByName("steinegger"));
            Assert.AreSame(theUser, mockUserRepository.GetUserByName("ayende"));
        }
Test_Complex_Expressions_With_Lambda_Expressions
[Test]
        public void Test_Complex_Expressions_With_Constraints()
        {
            //arrange
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();
            var theUser = new User() {Name = "ayende"};

            stubUserRepository.Stub(
                repository =>
                repository.GetUserByName(
                    Arg<string>.Matches(Rhino.Mocks.Constraints.Text.StartsWith("aye") ||
                                        Rhino.Mocks.Constraints.Text.StartsWith("stein")))).Return(theUser);

            //act ,assert
            Assert.AreSame(theUser, stubUserRepository.GetUserByName("steinegger"));
            Assert.AreSame(theUser, stubUserRepository.GetUserByName("ayende"));
        }
Test_Complex_Expressions_With_Constraints

Out和Ref參數

Ref和Out參數都很特殊,由於你必須使得編譯器運行順暢。關鍵字refout都是受委託的,而且你必須指定一個字段做爲參數。Arg不會讓你失望。

[Test]
        public void Test_Out_Argument_Constraints()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();

            User theUser;

            var user = new User() {Name = "ayende"};

            stubUserRepository.Stub(
                repository => repository.TryGetValue(Arg.Is("ayende"), out Arg<User>.Out(user).Dummy))
                              .Return(true);

            var rntvalue = stubUserRepository.TryGetValue("ayende", out theUser);

            Assert.True(rntvalue);
            Assert.IsNotNull(theUser);
            Assert.AreSame(user, theUser);
        }
Test_Out_Argument_Constraints

對於編譯器,out是受委託的。Arg<User>.Out(user)也是很重要的一部分,它指定了out參數應該返回user。Dummy是一個Arg<T>中T類型的一個字段,用於知足編譯器。

對於ref參數,基本上是相似的:

[Test]
        public void Test_Ref_Argument_Constraints()
        {
            var stubUserRepository = MockRepository.GenerateStub<IUserRepository>();

            stubUserRepository.Stub(
                repository =>
                repository.Foo(ref Arg<string>.Ref(Rhino.Mocks.Constraints.Is.Equal("Stefan"), "Oren").Dummy))
                              .Return(true);

            var userName = "Stefan";

            bool rntvalue = stubUserRepository.Foo(ref userName);

            Assert.True(rntvalue);
            Assert.AreEqual("Oren", userName);
        }
Test_Ref_Argument_Constraints

一樣,ref參數也是受委託的。Arg<string>.Ref(Rhino.Mocks.Constraints.Is.Equal("Stefan"), "Oren")定義了一個約束和一個返回值。一樣,Dummy也是Arg<T>的T類型的一個字段。

約束引用

Arg<T>.Is 
Equal(object), NotEqual(object)  使用Equals比較
GreaterThan(object), LessThan(object), LessThanOrEqual(object), GreaterThanOrEqual(object)  使用>,<,<=,>=操做符比較
Same(object), NotSame(object)  引用比較
Anything()  無約束
Null(), NotNull()  引用爲null或者不爲null
TypeOf(Type), TypeOf<T>()  參數爲肯定的類型
Matching<T>(Predicate<T>

參數匹配一個謂詞(詳見Arg<T>.Matches)。

IsIn(object) 枚舉的參數包含指定的對象
Arg<T>.List
OneOf(IEnumerable)  參數在指定的列表中
Equal(IEnumerable)  枚舉參數中的全部明細與指定參數中的明細進行比較
Count(AbstractConstraint)  參數的Count屬性的約束
Element(int, AbstractConstraint)  指定的索引處的元素知足約束
ContainsAll(IEnumerable)  枚舉參數至少包含指定的明細
Arg<T>.Property
AllPropertiesMatch(object)  全部的公開屬性與公開字段與指定對象的屬性進行比較。若是公開屬性或字段是複雜類型,比較將進行遞歸。
IsNotNull(string propertyName), IsNull(string propertyName)  給定名稱的屬性爲null或不爲null
Value(string propertyName, object expectedValue)  給定名稱的屬性等於預期的值
Value(string propertyName, AbstractConstraint constraint)  給定名稱的屬性知足約束
Arg.Text 
StartsWith(string), EndsWith(string), Contains(string)  字符串的時頭、尾或包含指定的字符串
Like(string regex)  字符串匹配正則表達式
Arg<T>.Matches 
Argt<T>.Matches(Expression)

 經過一個Lambda表達式來指定約束。如:

Arg<string>.Matches(s =>s.StartsWith("aye", StringComparison.InvariantCulture))

Argt<T>.Matches(AbstractConstraint) 直接指定約束,如:

Arg<string>.Matches(Rhino.Mocks.Constraints.Text.StartsWith("aye"))

如何發出事件

Rhino Mocks3.5引入了一種新的方式來從模擬對象發出事件。在3.5以前,通常都是經過IEventRaiser接口實現的:

[Test]
public void RaisingEventOnView()
{
   var mocks = new MockRepository();
   IView view = mocks.CreateMock<IView>();
   view.Load+=null;//create an expectation that someone will subscribe to this event
   LastCall.IgnoreArguments();// we don't care who is subscribing
   IEventRaiser raiseViewEvent = LastCall.GetEventRaiser();//get event raiser for the last event, in this case, View

   mocks.ReplayAll();

   Presenter p = new Presenter(view);
   raiseViewEvent.Raise();

   Assert.IsTrue(p.OnLoadCalled);   
}
RaisingEventOnView

而3.5提供的擴展方法,使得實現這點變得更簡單:

[Test]
public void RaisingEventOnViewUsingExtensionMethod()
{
   var mocks = new MockRepository();
   IView view = mocks.DynamicMock<IView>();
   Presenter p = new Presenter(view);

   view.Raise(x => x.Load += null, this, EventArgs.Empty);

   Assert.IsTrue(p.OnLoadCalled);   
}
RaisingEventOnViewUsingExtensionMethod

Raise()擴展方法傳入一個你想要發出的事件的捐助,事件發送者和事件參數。

屬性Setters顯式預期API

Rhino Mocks3.5添加了2個構想用於預期設置屬性。在以前的Rhino Mocks也是能夠的,可是沒有在流接口語法中表現出來。

用一個肯定值來對屬性設置預期:

Expect.Call(mockedCustomer.Name).SetPropertyWithArguments("Ayende");

對於交互測試,不指定值來設置屬性預期時這樣處理:

Expect.Call(mockedCustomer.Name).SetPropertyAndIgnoreArguments();

而用新的構想來處理:

using(mocks.Record()) { 
  mockedCustomer.Name = "Ayende"; // 設置指定參數來設定一個預期   LastCall.IgnoreArguments(); //針對參數設置的預期,不論實際值是什麼,忽略預期的參數 }

直接訪問方法參數

對於一些高級的場合,與其設置預期或斷言特定值,還不如直接處理方法調用來得更容易。

Rhino Mocks經過WhenCalled方法來支持這些功能。WhenCalled可干預模擬行爲,示例以下:

[Test]
        public void Test_Access_Method_Argument_Directly()
        {
            var wasCalled = false;

            var stub = MockRepository.GenerateStub<IView>();

            stub.Stub(view => view.StringAndString(Arg.Is("")))
                .Return("blah")
                .WhenCalled(delegate { wasCalled = true; });

            Assert.AreEqual("blah", stub.StringAndString(""));
            Assert.True(wasCalled);
        }
Test_Access_Method_Argument_Directly

上面的例子只展現了在方法調用時獲得通知,實際上WhenCalled有着更強大的功能。來看下面的例子:

[Test]
        public void Test_Modify_Method_Return_Value_By_WhenCalled()
        {
            var stub = MockRepository.GenerateStub<IView>();

            stub.Stub(view => view.StringAndString(Arg.Is("")))
                .Return("blah")
                .WhenCalled(invocation => invocation.ReturnValue = "haha");

            Assert.AreEqual("haha", stub.StringAndString(""));
        }
Test_Modify_Method_Return_Value_By_WhenCalled
[Test]
        public void Test_Inspect_Method_Argument_By_WhenCalled()
        {
            var stub = MockRepository.GenerateStub<IView>();

            stub.Stub(view => view.StringAndString(Arg<string>.Is.Anything))
                .Return("blah")
                .WhenCalled(invocation => Assert.AreNotEqual("", invocation.Arguments[0]));

            Assert.AreEqual("blah",stub.StringAndString("Stefan"));
        }
Test_Inspect_Method_Argument_By_WhenCalled

如你所見,WhenCalled方法的功能是很強大的,因此使用時要謹慎。

 

本文源於我的翻譯,敬請指正!

原文地址:Rhino Mocks 3.5

相關文章
相關標籤/搜索