单元测试
在敏捷的开发理念中,覆盖全面的自动化测试是添加新特性和重构的必要前提。单元测试在软件开发过程中的重要性不言而喻,特别是在测试驱动开发的开发模式越来越流行的前提下,单元测试更成为了软件开发过程中不可或缺的部分。同时单元测试也是提高软件质量,花费成本比较低的重要方法。
1.单元测试的时机和测试点
1.1单元测试的时机
- 在业务代码前编写单元测试采用测试驱动开发,这是我们经常使用和推荐的。
- 在业务代码过程中进行单元测试,对重要的业务逻辑和复杂的业务逻辑进行添加测试。
- 在业务逻辑之后再编写测试是我们不建议的,除非对遗留代码的修改,需要先进行测试用例的添加,保证我们修改和重构后的代码不会破坏之前的业务逻辑。
1.2单元测试的测试点
- 在逻辑复杂的代码中添加测试。
- 在容易出错的地方添加测试。
- 不易理解的代码中添加测试,在以后看到测试就可以非常清楚代码要实现的逻辑。
- 在考虑后期需求变更相对较大的代码中添加测试,这样后期需求更变修改代码之后就不用太担心写的代码对不对以及是否破坏了已有代码逻辑。
- 外部接口处添加解耦代码、同时增加单元测试。
2.代码不可测试性的根源
- 代码中调用到了底层平台的接口或只有系统运行后才能获得的资源(数据库连接、发送邮件,网络通讯,远程服务, 文件系统等)但业务代码与这些资源未解耦。这样在测试代码需要创建这个类的时候会去初始化这些资源时导致无法测试。
- 在方法内部new一个与本次测试无关的对象。
- 代码依赖层次很深,逻辑复杂,一次方法的往往要调用N次底层的接口,或者类的方法非常多。这样的代码我们需要对类进行重构,尽量保证类的单一职责:这个类在系统中的意图应当是单一的,且修改它的原因应该只有一个。
- 使用单例类和静态方法,并且单例类和静态方法使用到了我们底层的接口或者其他接口。
3.测试工具使用和测试方法介绍
在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,如调用平台接口、连接数据库、网络通讯、远程服务、FTP、文件系统等等。 而我们没法控制这些外部依赖的对象,为了解决这个问题,我们就需要用到Mock工具来模拟这些外部依赖的对象,来完成单元测试。 现在比较流行的Mock工具有JMock、EasyMock、Mockito、PowerMock。我们使用的是Mockito和PowerMock。PowerMock弥补了其他3个Mock工具不能mock静态、final 、私有方法的缺点。 在下面的情况下我们可以使用Mock对象来完成单元测试。
- 实对象具有不可确定的行为,会产生不可预测的结果。 如:数据库查询可以查出一条记录、多条记录、或者返回数据库异常等结果。
- 真实对象很难被创建。如:平台代码,或者Web、JBoss容器等。
- 真实对象的某些行为很难触发。 如:代码中需要处理的网络异常、数据库异常、消息发送异常等。
- 真实情况令程序运行很慢。 在敏捷的实践中我们完成了CI,在开发提交代码前需要执行整个项目的单元测试用例,只有测试通过才可以提交代码。这就要求我们每个单元测试用例需要尽可能的短,整个项目的测试时间才会短。当有的测试用例需要测试大数据量情况下系统的预期时,就需要使用Mock对象。
如我们代码中需要判断只有当系统的缓存队列大于40000时,我们开始考虑丢弃非关键的消息,当超过48000时,需要只处理最重要的消息,当超过50000时需要丢弃全部消息。此时就需要对此缓存队列进行Mock,根据调用返回不同的数据量给测试。 5. 测试需要知道真实对象是如何被调用的。如:测试用例需要验证是否发送了JMS,此时就可以通过Mock对象是否被调用来测试。 6. 真实对象实际不存在时。 如:当我们与其他模块交互时,或者与新的接口打交道时,更有就是对方的代码还没有开发完毕时,我们可以通过Mock来模拟接口的行为,实现代码逻辑的验证和测试。
3.1 Mocktio简单使用说明
mock可以模拟各种各样的对象,从而代替真正的对象做出希望的响应。
1、模拟对象的创建
List cache = mock(ArrayList.class); System.out.println(cache.get(0)); //-> null 由于没有对mock对象给预期,所以返回都是null
2、模拟对象方法调用的返回值
List cache = mock(ArrayList.class); when(cache.get(0)).thenReturn("hello"); System.out.println(cache.get(0)); //-> ello
3、模拟对象方法多次调用和多次返回值
List cache = mock(ArrayList.class); when(cache.get(0)).thenReturn("0").thenReturn("1").thenReturn("2"); System.out.println(cache.get(0)); System.out.println(cache.get(0)); System.out.println(cache.get(0)); System.out.println(cache.get(0)); //-> 0,1,2,2 如果实际调用的次数超过了预期的次数,则会一直返回最后一次的预期值。
4、模拟对象方法调用抛出异常
List cache = mock(ArrayList.class); when(cache.get(0)).thenReturn(new Exception("Exception")); System.out.println(cache.get(0));
5、模拟对象方法在没有返回值时也可以抛异常
List cache = mock(ArrayList.class); doThow(new Exception("Exception")).when(cache).clear();
6、模拟方法调用时的参数匹配
AnyInt的使用,匹配任何int参数 List cache = mock(ArrayList.class); when(cache.get(anyInt())).thenReturn("0"); System.out.println(cache.get(0)); System.out.println(cache.get(2)); //-> 0,0
7、模拟方法是否被调用和调用的次数,预期调用了一次
List cache = mock(ArrayList.class); cache.add("steven"); verify(cache).add("steven"); 复制代码
预期调用了两次入缓存,没有调用清除缓存的方法
List cache = mock(ArrayList.class); cache.add("steven"); cache.add("steven"); verify(cache,times(2)).add("steven"); verify(cache,never()).clear();
还可以通过atLeast(int i)和atMost(int i)来替代times(int i)来验证被调用的次数最小值和最大值。【注意】Mock对象默认情况下,对于所有有返回值且没有预期过的方法,Mocktio会返回相应的默认值。对于内置类型会返回默认值,如int会返回0,布尔值返回false。对于其他type会返回null。mock对象会覆盖整个被mock的对象,因此没有预期的方法只能返回默认值。这个在初次使用Mock时需要注意,经常会发现测试结果不对,最后才发现自己未给相应的预期。
3.2 PowerMock简单使用说明
PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。 PowerMock使用简单,在类名前添加注解,在预期前调用PowerMock的mock静态类方法,其他的预期方法和Mockito类似。
@PrepareForTest(System.class) @RunWith(PowerMockRunner.class) public class Test { @org.junit.Test public void should_get_filed() { System.out.println(System.getProperty("myName")); PowerMockito.mockStatic(System.class); PowerMockito.when(System.getProperty("myName")).thenReturn("steven"); System.out.println(System.getProperty("myName")); //->null steven } }
3.3 Fake对象的使用
测试中需要模拟对象,除了常用的mock对象外,我们还会经常用到Fake对象。Mock对象是预先计划好的对象,带有各种期待,他们组成了一个关于他们期待接受的调用的详细说明。而Fake对象是有实际可工作的实现,但是通常有一些缺点导致不适合用于产品,我们通常使用Fake对象在测试中来模拟真实的对象。 在测试中经常会发现我们需要使用系统或者平台给我们提供的接口,在测试中我们可以新创建一个类去实现此接口,然后在根据具体情况去实习此模拟类的相应方法。
如我们创建了自己的FakeLog对象来模拟真实的日志打印,这样我们可以在测试类中使用FakeLog来代替代码中真实使用的Log类,可以通过FakeLog的方法和预期的结果比较来进行测试正确性的判断。
Fake对象和mock对象还有一个实际中使用的区别,Fake对象我们构造好后,以后所有的代码都去调用此Fake对象就可以了,不用每个类每次都要给预期。从这个角度可以看到当一个类的方法或者预期相对不变时,可以采用Fake对象,当这个类的返回信息预期变化非常不可预期时,可以采用MOCK对象。
3.4Mock服务的两种方式
(1)直接注入:用于类之间的依赖层次较多的情况,测试整个业务流程,粒度大。
Service service = mock(Service.class); new Processor().process(service ); 复制代码
(2)重写protected方法返回mock对象:用于类直接依赖于该服务的情况,测试行为的细节,粒度小。
Service service = mock(Service .class); generator = new Generator() { @Override protected Service getService() { return service; } }
3.5测试异常
Throwable有两个直接子类:Exception和Error
1、expcetd=SomeExecption.class
@Test(expected = AssertionError.class) public void should_occur_assertion_error_when_xx() throws Exception { new Processor().process(); } @Test(expected = NumberFormatException.class) public void should_throw_number_format_exception_when_xx() { Convert.convert2Long(); } 复制代码
2、try-catch-fail只能用于Exception,Error不能用此种方式
try { method.invoke(); fail(); } catch (Exception e) { assertTrue(e.getCause() instanceof RuntimeException); }
3.6私有方法—采用反射来调用
@Test public void should_throw_runtime_exception_when_check_data_fail() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method method = Generator.class.getDeclaredMethod("check", Item.class); method.setAccessible(true); try { method.invoke(Generator, mock(Item.class)); } catch (Exception e) { assertTrue(e.getCause() instanceof RuntimeException); } }
4.单元测试的格式
4.1测试类结构
public class ExampleTest { @BeforeClass public static void setUp() throws Exception { initGlobalParameter(); registerServices(); } @Before public void setUp() throws Exception { initGlobalParameter(); registerServices(); } @After public void tearDown() throws Exception { ServiceAccess.serviceMap.clear(); clearCache(); } @AfterClass public static void tearDown() throws Exception { ServiceAccess.serviceMap.clear(); clearCache(); } @Test public void should_get_some_result1_when_give_some_condition1{ } @Test public void should_get_some_result2_when_give_some_condition2{ } }
JUnit4是JUnit框架有史以来的最大改进,其主要目标便是利用Java5的Annotation特性简化测试用例的编写。先简单解释一下什么是Annotation,这个单词一般是翻译成元数据。元数据是什么?元数据就是描述数据的数据。也就是说,这个东西在Java里面可以用来和public、static等关键字一样来修饰类名、方法名、变量名。修饰的作用描述这个数据是做什么用的,差不多和public描述这个数据是公有的一样。
- @Before:每个测试方法执行之前都要执行一次。
- @After:before对应,每个测试方法执行之后要执行一次。
- @BeforeClass:在所有测试方法之前运行,只运行一次。一般在此类中申请昂贵的外部资源。父类中有@BeforeClass方法,在其子类运行之前也会运行。
- @AfterClass:与BeforeClass对应,在所有测试结束后,释放BeforeClass中申请的资源。 注意:@Before,@After,@BeforeClass,@AfterClass 标示的方法一个类中只能各有一个
- @Test: 告诉JUnit,该方法要作为一个测试用例来运行。
4.2测试代码的位置
在Java中一个包可以横跨两个不同的目录,所以我们的测试代码和产品代码放在同一目录中,这样维护起来更方便,测试代码和产品代码在同一个包中,这样也减少了不必要的包引起,同时在测试类中使用继承更加的方便。
4.3测试用例格式3段式
一个测试用例主体内容一般采用三段式:given-when-then
- Given:构造测试条件;
- When:执行待测试的方法;
- Then:判断测试结果是否符合期望。
例如:
@Test public void should_get_correct_result_when_add_two_numbers() { int a = 1; int b = 2; int c = MyMath.add(a, b); assertEquals(3, c); }
4.4类名的命名方式
测试类的名称以Test结尾。从目标类的类名衍生出其单元测试类的类名。类名前加上Test后缀。 Fake(伪类)放在测试包中,使用前缀Fake。
4.5方法名的定义方式
should …do something…when…under some conditions…
例如:
should_NOT_delete_A_when_exists_B_related_with_A should_throw_exception_when_the_parameter_is_illegal
4.6业务代码中为测试提供的方法的注解
在业务代码中为了测试而单独提供的保护方法或者其他方法,我们通过@ForTest来标注。FofTest类如下:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) public @interface ForTest { String description() default ""; }