本文主要介绍Visual Studio(2012+)单元测试框架的一些技巧:
- 如何模拟类的静态构造函数
- 如何测试某方法被调用过
- 如何测试某方法执行的次数
- 并行编程测试注意事项
一、如何模拟类的静态构造函数
1.1 被测代码
namespace BlogDemo.UTDemo.Tricks { public class StaticConstructorExample { static StaticConstructorExample() { //step1:read data from web service //step2:save data into database } public int DoSomething() { return 0; } } }
上面的类有一个静态构造函数,里面做两件事情:
- 从某webservice读取数据
- 将读取到的数据保存到数据库。
以上两件事情都是对外界环境的严重依赖
我们需要测试的方法是DoSomething,需要测试这个类,就需要实例化StaticConstructorExample这个类,实例化这个类之前,其静态构造函数将会被自动执行:这个点就是问题。
- 静态构造函数依赖外部环境,这个外部环境有可能在某一天就挂了
- 如果外部环境挂了,静态构造函数就会报错,那我们的单元测试就无法按照预期进行运行
- DoSOmething本身不直接依赖外部环境,但是通过静态构造函数间接依赖了外部环境,这违反了单元测试repeatable原则(当外部环境挂了,我们的测试就无法运行)
解决办法依然是Shim,通过ms.test的Shim可以模拟一个类的静态构造函数,从而改变静态构造函数的行为,消除外部依赖。
1.2 测试代码
[TestClass] public class StaticConstructorExampleTests { [TestMethod] public void DoSomethingTest() { using (ShimsContext.Create()) { var isStaticConstructorExecuted = false; ShimStaticConstructorExample.StaticConstructor = () => { //step1:mock web service //step2:mock database isStaticConstructorExecuted = true; }; var example = new StaticConstructorExample(); var data = example.DoSomething(); Assert.AreEqual(0, data); Assert.IsTrue(isStaticConstructorExecuted); } } }
上面的代码是通过ShimClassName.StaticConstructor来实现对静态构造函数进行模拟的。
二、如何测试某方法被调用过
2.1 被测代码
基础代码,在DemoClass中进行调用
public interface IGoodActionHandler { void Action(); } public interface IBadActionHandler { void Action(); } public class GoodHandler : IGoodActionHandler { public void Action() { throw new NotImplementedException(); } } public class BadHandler : IBadActionHandler { public void Action() { throw new NotImplementedException(); } }
要被测试的类:
public class DemoClass { IGoodActionHandler goodHandler; IBadActionHandler badHandler; public DemoClass() { this.goodHandler = new GoodHandler(); this.badHandler = new BadHandler(); } public void DoSomething(int type) { if (type == 1) { DoSomethingGood(); } else { DoSomethingBad(); } } private void DoSomethingGood() { this.goodHandler.Action(); } private void DoSomethingBad() { this.badHandler.Action(); } }
要测试上面的DoSomething方法,上面DoSomethingGood和DoSomethingBad都依赖自接口,在测试的时候都可以进行mock(stub)。在侧测DoSomething方法是只需要验证当Type为1时执行了goodHandler的action,否则执行badHandler的Action。
这也是单元测试的一个关键点:关注单元。这里不关注goodhandler 和badhandler的内部逻辑(这两个handler的内部逻辑可以单独测试,属于另外的单元),这里只关注是否按照逻辑路由到了正确的handler。
2.2测试代码
[TestClass] public class DemoClassTests { [TestMethod] public void DoSomething_DoSomethingGood_Tests() { var goodHandlerExecuted = false; StubIGoodActionHandler goodHandler = new StubIGoodActionHandler() { Action = () => { goodHandlerExecuted = true;//如果执行了goodhandler则置为true } }; var badHandlerExecuted = false; StubIBadActionHandler badHandler = new StubIBadActionHandler() { Action = () => { badHandlerExecuted = true;//如果执行了badhandler则置为true } }; var demoClass=new DemoClass(); demoClass.BadHandler=badHandler;//注入badhandler demoClass.GoodHandler=goodHandler;//注入goodhandler demoClass.DoSomething(1); Assert.IsTrue(goodHandlerExecuted);//执行了goodhandler Assert.IsFalse(badHandlerExecuted);//没有执行badhandler } }
上面使用了两个技术
- 面向接口编程,使用Stub技术,动态注入Stub(打桩),在测试的时候对对象进行了模拟
- 改变了Stub对象的行为,本文只是通过在Stub对象的内部设置标志位的值来表示是否执行了这一个步骤。
上面的标志位的方法Shim技术照样使用,静态构造函数的模拟就是通过Shim后在构造函数内部修改标识位来验证构造函数被执行的。接下来介绍的测试执行次数也是通过shim后修改标识位来实现的
三、如何测试某方法执行的次数
3.1 被测代码
public class DemoClass { private LoopHandler handler; public DemoClass() { handler = new LoopHandler(); } public void DoSomething(int times) { for (var i = 0; i < times; i++) { handler.Do(); } } } public class LoopHandler { public void Do() { } }
现在要验证DoSomething是否执行了times次Do方法。
3.2 测试代码
[TestMethod] public void DoSomething_RunTimes_Test() { using(ShimsContext.Create()) { var times = 0; ShimLoopHandler.AllInstances.Do = (@this) => { times++;//每进入一次加一次 }; var givenTimes = 100; new DemoClass().DoSomething(givenTimes); Assert.AreEqual(times, givenTimes); } }
思想和上面Stub是一样,在Shim模拟的方法内部进行计数器相加。
3.3 并行编程测试注意事项
这里有一点需要注意一下,因为int的++不是线程安全的,如果把DemoClass的循环修改成多个线程并行执行的话测试代码需要做相应的调整。,对times++进行lock。
public void DoSomething(int times) { Parallel.For(0, times, (item) =>//并行执行 { handler.Do(); }); } [TestMethod] public void DoSomething_RunTimes_Test() { using(ShimsContext.Create()) { var times = 0; ShimLoopHandler.AllInstances.Do = (@this) => { lock (this)//lock线程安全 { times++;//每进入一次加一次 } }; var givenTimes = 100; new DemoClass().DoSomething(givenTimes); Assert.AreEqual(times, givenTimes); } }
上面的代码当Times不是特别大的时候一般不加lock也是可以的,不是特别大的时候一般不会产生线程安全问题。但是当times比较大的时候不加lock就会出问题。
当times是100000时去掉lock。我运行就出现了问题:
测试就是为了严谨,如果有上面并行的问题,测试的时候还是加上lock比较好。