在dotnet环境中,我们通常使用NUnit和RhinoMocks来编写单元测试。
NUnit 简介
NUnit是.net平台上的一个单元测试框架,用来帮助开发人员方便的完成单元测试的编写。其主页为
http://www.nunit.org,目前最新版本为2.6.3.
可以通过以下链接来查看不同版本的使用文档
http://www.nunit.org/index.php?p=documentation
Rhino Mocks简介
Rhino Mocks是一个模拟对象创建和管理框架。使用它,开发人员可以方便地模拟并断言依赖对象被调用的情况。
下载地址:http://hibernatingrhinos.com/downloads/rhino-mocks/latest
如何编写测试用例
创建一个测试工程,命名为{被测试工程}.UnitTest, 并添加NUnit和RhinoMocks引用。把项目属性中Application 面板中的Outputtype设置为Class Library
对应的dll 分别nunit.framework.dll和Rhino.Mocks.dll
创建测试类,命名为{被测试类}Test,测试类的目录结构和命名空间保持和被测试类一致。
在测试类中引入命名空间NUnit.Framework和Rhino.Mocks
using NUnit.Framework;
using Rhino.Mocks;
在测试类的summary注解中,列出测试列表,并把测试类使用[TestFixture]来标注
[TestFixture] class SchoonerStorageTest { }
针对测试列表来建立测试用例,并使用[Test]来标注,测试用例采用Test_{TestedMethod}_{测试点}_{预期结果}来命名,测试过程分三步走
[Test]
public void Test_{TestedMethod}_{测试点}_{预期结果}()
{
测试过程分三步:
//准备测试数据
//执行被测试方法
//验证期望结果
}
单元测试中的NUnit
测试用例执行过程
[TestFixture] public class TestFixtureSetUpAndTearDownTest { [TestFixtureSetUp] public void RunBeforeAllTests() { Console.WriteLine("TestFixtureSetUp"); } [TestFixtureTearDown] public void RunAfterAllTests() { Console.WriteLine("TestFixtureTearDown"); } [SetUp] public void RunBeforeEachTest() { Console.WriteLine("SetUp"); } [TearDown] public void RunAfterEachTest() { Console.WriteLine("TearDown"); } [Test] public void Test1() { Console.WriteLine("Test1"); } }
TestFixtureSetUp(这个测试类启动时被调用一次)
SetUp(每个测试用例启动时被调用一次)
Test(执行测试用例)
TearDown(每个测试用例结束时被调用一次)
TestFixtureTeardDown(这个测试类结束时被调用一次)
断言使用(Assertions)
有两种断言模型
经典模型(classic model)
相等断言(Assert.AreEqual, Assert.AreNotEqual...)
引用相等断言(Assert.AreSame, Assert.AreNotSame...)
比较断言(Assert.Greater, Assert.Less ...)
类型断言(Assert.IsInstanceOfType, Assert.IsAssignableFrom...)
条件断言 (Assert.True, Assert.False, Assert.IsNan, Assert.IsNull, Assert.IsEmpty)
Assert类中有用的方法(这些方法运行你对测试进程进行控制)
Assert.Pass:使测试用例成功
The Assert.Pass method allows you to immediately end the test, recording it as successful. Since it causes an exception to be thrown, it is more efficient to simply allow the test to return. However, Assert.Pass allows you to record a message in the test result and may also make the test easier to read in some situations. Additionally, like the other methods on this page, it can be invoked from a nested method call with the result of immediately terminating test execution.
Assert.Fail:使测试用例失败
The Assert.Fail method provides you with the ability to generate a failure based on tests that are not encapsulated by the other methods. It is also useful in developing your own project-specific assertions.
Assert.Ignore:忽略测试用例
The Assert.Ignore method provides you with the ability to dynamically cause a test or suite to be ignored at runtime. It may be called in a test, setup or fixture setup method. We recommend that you use this only in isolated cases. The category facility is provided for more extensive inclusion or exclusion of tests or you may elect to simply divide tests run on different occasions into different assemblies.
Assert.Inconclusive:测试用例使用现有测试数据不能正常完成(不是太理解这个方法的应用场景)
The Assert.Inconclusive method indicates that the test could not be completed with the data available. It should be used in situations where another run with different data might run to completion, with either a success or failure outcome.
DirectoryAssert类
FileAssert类
CollectionAssert类
StringAssert类
基于约束的模型(constraint-based model)
可以自定义约束来实现比较复杂的断言
Assert.That(..)
属性使用(Attributes)
测试异常发生
[Test(Description = "If the Id or Name attribute is not found in xpath 'StorageManagement/WritePolicy/CategoryConfig/CategoryList/Category', an exception will be thrown.")] [ExpectedException(ExpectedException = typeof(ConfigException), MatchType = MessageMatch.Contains, ExpectedMessage = "Id, Name attributes are required")] public void Test_LoadConfig_RequiredInfoNotFound_CategoryInWritePolicyCategoryList() { string confileFile = GetTestFile("CategoryStorageConfig_RequiredInfoNotFound_CategoryInWritePolicyCategoryList.xml"); categoryStorageConfig.LoadConfig(confileFile); Assert.Fail("Failure"); }
测试超时
[Test,Timeout(10*1000)] public void Test_WriteAndGetOneData() { ... }
测试用例分类
[Test,Timeout(10*1000)] [Category("FunctionTest")] public void Test_WriteAndGetMultipleData() { .... }
单元测试中的RhinoMocks
创建桩对象
桩对象不参与验证过程,其作用是使代码可以顺畅地运行到完成或运行到你希望测试的点。
例子
[Test] public void CreateStub_1() { MockRepository mocks = new MockRepository(); IAnimal animal = mocks.Stub<IAnimal>(); animal.Legs = 1; Assert.AreEqual(1, animal.Legs); }
创建模拟对象
模拟对象参与验证过程,其作用是为了检测对应的依赖对象是否被调用,以及调用的顺序是否正确等等
严格:
给模拟对象设定的期望必须被执行,没有设定的期望不能发生,否则调用Verify或VerifyAll时,会导致测试用例失败
例子
[Test] public void CreateStrictMock_1() { MockRepository mocks = new MockRepository(); ICustomerService customerService = mocks.StrictMock<ICustomerService>(); customerService.Expect(p => p.BuyBook(null)); mocks.ReplayAll(); customerService.BuyBook(null); mocks.VerifyAll(); } [Test] public void CreateStrictMock_2() { MockRepository mocks = new MockRepository(); ICustomerService customerService = mocks.DynamicMock<ICustomerService>(); using (mocks.Record()) { customerService.Expect(p => p.BuyBook(null)).Repeat.Any(); } customerService.BuyBook(null); customerService.BuyBook(null); customerService.BuyBook(null); mocks.VerifyAll(); } [Test] public void CreateStrictMock_3() { ICustomerService customerService = MockRepository.GenerateStrictMock<ICustomerService>(); customerService.Expect(p => p.BuyBook(null)); customerService.Expect(p => p.BuyTicket(null)); customerService.BuyBook(null); customerService.BuyTicket(null); customerService.VerifyAllExpectations(); } [Test] public void CreateStrictMock_ShouldBeFail() { MockRepository mocks = new MockRepository(); ICustomerService customerService = mocks.StrictMock<ICustomerService>(); using (mocks.Record()) { customerService.Expect(p => p.BuyBook(null)).Repeat.Any(); } customerService.BuyBook(null); customerService.BuyTicket(null);//BuyTicket is not recorded and it cannot be called. mocks.VerifyAll(); }
非严格:
给模拟对象设定的期望必须被执行,没有设定的期望可以发生,否则调用Verify或VerifyAll时,会导致测试用例失败
例子
[Test] public void CreateDynamicMock_1() { MockRepository mocks = new MockRepository(); ICustomerService customerService = mocks.DynamicMock<ICustomerService>(); customerService.Expect(p => p.BuyBook(null)); mocks.ReplayAll(); customerService.BuyBook(null); mocks.VerifyAll(); } [Test] public void CreateDynamicMock_2() { MockRepository mocks = new MockRepository(); ICustomerService customerService = mocks.DynamicMock<ICustomerService>(); using (mocks.Record()) { customerService.Expect(p => p.BuyBook(null)); } customerService.BuyBook(null); mocks.VerifyAll(); } [Test] public void CreateDynamicMock_3() { MockRepository mocks = new MockRepository(); ICustomerService customerService = mocks.DynamicMock<ICustomerService>(); using (mocks.Record()) { customerService.Expect(p => p.BuyBook(null)).Repeat.Any(); } customerService.BuyBook(null); customerService.BuyTicket(null);//Even though BuyTicket is not recorded and it be called. mocks.VerifyAll(); } [Test] public void CreateDynamicMock_4() { MockRepository mocks = new MockRepository(); IAnimal animal = mocks.DynamicMock<IAnimal>(); Expect.Call(animal.Legs).PropertyBehavior(); animal.Legs = 1; Assert.AreEqual(1, animal.Legs); }
创建部分模拟对象
部分模拟对象可以用于测试抽象类中的模板方法。
例子
[Test] public void CreatePartialMock_1() { MockRepository mocks = new MockRepository(); AbstractProcessor processor = mocks.PartialMock<AbstractProcessor>(); using (mocks.Record()) { Expect.Call(processor.GetStatus()).Return(0); Expect.Call(processor.GetStatus()).Return(1); } Assert.AreEqual("OK", processor.Process()); Assert.AreEqual("NOTOK", processor.Process()); mocks.VerifyAll(); }
public abstract class AbstractProcessor { public string Process() { if (GetStatus() == 0) { return "OK"; } else { return "NOTOK"; } } public abstract int GetStatus(); }
模拟对象属性调用
[Test] public void Property_1() { MockRepository mocks = new MockRepository(); Ticket ticket = mocks.StrictMock<Ticket>(); using (mocks.Record()) { Expect.Call(ticket.Movie).PropertyBehavior(); Expect.Call(ticket.Price).PropertyBehavior(); } ticket.Movie = "ABC"; ticket.Price = 10.0f; Assert.AreEqual("ABC", ticket.Movie); Assert.AreEqual(10.0f, ticket.Price); ticket.VerifyAllExpectations(); } [Test] public void Property_2() { MockRepository mocks = new MockRepository(); Ticket ticket = mocks.StrictMock<Ticket>(); using (mocks.Record()) { Expect.Call(ticket.Movie).Return("ABC"); Expect.Call(ticket.Price).Return(10.0f); } Assert.AreEqual("ABC", ticket.Movie); Assert.AreEqual(10.0f, ticket.Price); ticket.VerifyAllExpectations(); } [Test] public void Property_3() { MockRepository mocks = new MockRepository(); Book book = mocks.Stub<Book>(); book.Name = "ABC"; book.Price = 10.0f; Assert.AreEqual("ABC", book.Name); Assert.AreEqual(10.0f, book.Price); }
模拟delegate和设定模拟对象方法被调用的顺序
[Test] public void Ordered_OK() { MockRepository mocks = new MockRepository(); ICustomerService customer = mocks.StrictMock<ICustomerService>(); using (mocks.Ordered()) { customer.BuyTicket(null); customer.BuyBook(null); } customer.Replay(); customer.BuyTicket(null); customer.BuyBook(null); customer.VerifyAllExpectations(); } [Test] public void Ordered_ShouldFail() { MockRepository mocks = new MockRepository(); ICustomerService customer = mocks.StrictMock<ICustomerService>(); using (mocks.Ordered()) { customer.BuyTicket(null); customer.BuyBook(null); } customer.Replay(); customer.BuyBook(null); customer.BuyTicket(null); customer.VerifyAllExpectations(); } [Test] public void Delegate_1() { MockRepository mocks = new MockRepository(); var oo = mocks.DynamicMock<DoThing>(); oo("BuyBook"); oo.Replay(); oo("BuyBook"); oo.VerifyAllExpectations(); } [Test] public void Delegate_Action() { MockRepository mocks = new MockRepository(); var oo = mocks.DynamicMock<Action<string>>(); oo("BuyBook"); oo.Replay(); oo("BuyBook"); oo.VerifyAllExpectations(); } [Test] public void Delegate_Func() { MockRepository mocks = new MockRepository(); var oo = mocks.DynamicMock<Func<string,string>>(); Expect.Call(oo("BuyBook")).Return("OK"); oo.Replay(); Assert.AreEqual("OK", oo("BuyBook")); oo.VerifyAllExpectations(); } public delegate void DoThing(string thing); }
断言方法没有被调用
[Test] public void When_user_forgot_password_should_save_user() { var stubUserRepository = MockRepository.GenerateStub<IUserRepository>(); var stubbedSmsSender = MockRepository.GenerateStub<ISmsSender>(); User theUser = new User { HashedPassword = "this is not hashed password" }; stubUserRepository.Stub(x => x.GetUserByName("ayende")).Return(theUser); stubUserRepository.Stub(x => x.Save(theUser)); var controllerUnderTest = new LoginController(stubUserRepository, stubbedSmsSender); controllerUnderTest.ForgotMyPassword("ayende"); stubUserRepository.AssertWasCalled(x => x.Save(theUser)); stubbedSmsSender.AssertWasNotCalled(x => x.SendMessage(null)); }
对模拟对象设置期望
[Test] public void Method_NoReturnValue() { MockRepository mocks = new MockRepository(); ICustomerService customer = mocks.StrictMock<ICustomerService>(); using (mocks.Record()) { customer.BuyBook(null); } } [Test] public void Method_HasReturnValue() { MockRepository mocks = new MockRepository(); var func = mocks.StrictMock<Func<string,string>>(); using (mocks.Record()) { Expect.Call(func("params")).Return("Result"); } } [Test] public void Method_OutParameter() { MockRepository mocks = new MockRepository(); var func = mocks.DynamicMock<DoThing1>(); string strOut = "ABC"; using (mocks.Record()) { Expect.Call(func("", out strOut)).Return("Result").OutRef("xxx"); } mocks.ReplayAll(); Assert.AreEqual("Result", func("", out strOut)); Assert.AreEqual("xxx", strOut); mocks.VerifyAll(); } [Test] public void Method_RefParameter() { MockRepository mocks = new MockRepository(); var func = mocks.DynamicMock<DoThing2>(); string strRef = "ABC"; using (mocks.Record()) { Expect.Call(func("", ref strRef)).Return("Result").OutRef("xxx"); } mocks.ReplayAll(); Assert.AreEqual("Result", func("", ref strRef)); Assert.AreEqual("xxx", strRef); mocks.VerifyAll(); } [Test] public void Method_Options() { MockRepository mocks = new MockRepository(); var func = mocks.DynamicMock<DoThing>(); using (mocks.Record()) { //返回值 Expect.Call(func("")).Return(""); //异常 Expect.Call(func("")).Return("").Throw(new Exception()); //方法允许使用的次数 Expect.Call(func("")).Return("").Repeat.Any(); Expect.Call(func("")).Return("").Repeat.Once(); Expect.Call(func("")).Return("").Repeat.Twice(); Expect.Call(func("")).Return("").Repeat.Times(3); //忽略方法参数 Expect.Call(func("")).Return("").IgnoreArguments(); } } public delegate string DoThing(string s1); public delegate string DoThing1(string s1, out string outs1); public delegate string DoThing2(string s1, ref string ref1);
设置期望时使用约束
[Test] public void Constraints_Is_1() { MockRepository mocks = new MockRepository(); var customer = mocks.DynamicMock<ICustomerService>(); Expect.Call(customer.ShowTitle("")) .Return("字符约束") .Constraints(Rhino.Mocks.Constraints.Is.Matching<string>(x => x.StartsWith("cnblogs"))); mocks.ReplayAll(); Assert.AreEqual("字符约束", customer.ShowTitle("cnblogs my favoured")); } [Test] public void Constraints_Property_OK() { MockRepository mocks = new MockRepository(); var customer = mocks.DynamicMock<ICustomerService>(); using (mocks.Record()) { customer.BuyBook(null); LastCall.On(customer).Constraints(Rhino.Mocks.Constraints.Property.Value("Name", "ABC")); } mocks.ReplayAll(); customer.BuyBook(new Book { Name = "ABC" }); mocks.VerifyAll(); } [Test] public void Constraints_Property_ShouldFail() { MockRepository mocks = new MockRepository(); var customer = mocks.DynamicMock<ICustomerService>(); using (mocks.Record()) { Expect.Call(delegate { customer.BuyBook(null); }).Constraints(Rhino.Mocks.Constraints.Property.Value("Name", "ABC")); } mocks.ReplayAll(); customer.BuyBook(new Book { Name = "DDDABC" }); mocks.VerifyAll(); } [Test] public void Constraints_List_OK() { MockRepository mocks = new MockRepository(); var customer = mocks.DynamicMock<ICustomerService>(); Expect.Call(customer.SumPrice(null)) .Return(100) .Constraints(Rhino.Mocks.Constraints.List.Equal(new int[]{100})); mocks.ReplayAll(); Assert.AreEqual(100, customer.SumPrice(new int[]{100})); } [Test] public void Constraints_List_ShouldFail() { MockRepository mocks = new MockRepository(); var customer = mocks.DynamicMock<ICustomerService>(); Expect.Call(customer.SumPrice(null)) .Return(100) .Constraints(Rhino.Mocks.Constraints.List.Equal(new int[] { 10 })); mocks.ReplayAll(); Assert.AreEqual(100, customer.SumPrice(new int[] { 100 })); } [Test] public void Constraints_Text() { MockRepository mocks = new MockRepository(); var customer = mocks.DynamicMock<ICustomerService>(); Expect.Call(customer.ShowTitle("")) .Return("字符约束") .Constraints(Rhino.Mocks.Constraints.Text.StartsWith("cnblogs") && Rhino.Mocks.Constraints.Text.EndsWith("!")); mocks.ReplayAll(); Assert.AreEqual("字符约束", customer.ShowTitle("cnblogs my favoured!")); }
设置期望时返回动态值(Do扩展方法的应用)
[Test] public void SayHelloWorld() { MockRepository mocks = new MockRepository(); INameSource nameSource = mocks.DynamicMock<INameSource>(); Expect.Call(nameSource.CreateName(null, null)) .IgnoreArguments() .Do(new NameSourceDelegate(Formal)); mocks.ReplayAll(); string expected = "Hi, my name is Ayende Rahien"; string actual = new Speaker("Ayende", "Rahien", nameSource).Introduce(); Assert.AreEqual(expected, actual); } delegate string NameSourceDelegate(string first, string surname); private string Formal(string first, string surname) { return first + " " + surname; }
创建模拟对象对象注意事项:
如果是类,必须包含无参数构造函数,被模拟的方法必须是虚方法;
无法模拟静态类和静态方法;
尽量通过被测试类的属性来设置被模拟对象(少用构造函数来建立依赖);
如何运行测试用例
方式1(从VS中启动nunit.exe,可以对测试代码和产品代码进行调试):
在项目属性中的Debug面板中选中start external program并设置为{NUnitRoot}\bin\nunit.exe,然后启动测试工程。启动之后,打开测试工程对应的dll(一般是在*\bin\Debug\下)
运行你希望执行的测试用例
方式2
打开{NUnitRoot}\bin\nunit.exe,并打开测试工程对应的dll来运行你希望执行的测试用例
方式3(这个方式多用于持续集成环境中)
通过nunit-console.exe来运行,你必须在参数中指定你要运行dll,测试类,和输出结果的位置等相关参数