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

相关文章
相关标签/搜索