JUnit4 使用要点

Android 单元测试默认使用 JUnit4,和其他的 Java 项目使用的测试方案区别不大,这里就对 JUnit4 的使用要点进行总结,详细内容应查询官方培训的测试教程

基本用法

JUnit4 的测试代码一般都放在 src/main/test 目录下,在 Android 项目中负责进行本地单元测试。

依赖关系的设置代码为(版本号应参考最新版本说明):

1
testCompile 'juint:junit:4.12'

最简单的测试方法就是最任意有测试内容的方法标记注解 @Test,而后 IDE 会自动识别测试方法。测试方法内容编写和普通方法并无不同,只是其中有一系列的 assert 开头的方法,用来判断测试结果。比如:

1
2
3
4
5
6
7
8
public class EmailValidatorTest {

    @Test
    public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
        assertThat(EmailValidator.isValidEmail("name@email.com"), is(true));
    }
    ...
}

Hamcrest 是一个通用的匹配器构造和检测库,有多个编程语言的版本,Java 版的官网地址为:http://hamcrest.org/JavaHamcrest/ Android Studio 会自动为 Android 项目添加这个库的依赖,无需手动管理。一般使用这个库,是为了扩展断言的用法,同时也能提供可读性更高的断言语句。比如:

1
2
3
ClassA a = new ClassA();
ClassA b = new ClassA();
assertThat(a, equalTo(b));

一般常使用 assertThat 方法进行断言,第一个参数为需要断言的结果,第二个参数为断言对应的匹配器或匹配模式。 常用的匹配器有:

  • anything:无论结果如何,总是匹配
  • equalTo:相等(使用 Object.equals 判断)
  • is:equalTo 的语法糖
  • not:不匹配,内部可包裹匹配器,相当与逻辑符 !
  • allOf:必须全部匹配参数中的匹配器,最后结果才能匹配成功
  • anyOf:任一参数匹配即可匹配成功
  • notNullValue:结果非空
  • nullValue:结果为 null

其他的匹配器用法可以参考官方文档 熟练的使用匹配器可以大幅减少测试代码的编写,尤其是复杂的逻辑代码和结果判断代码,也防止了测试代码本身出错的情况。

如果测试中需要依赖某些测试对象之外的代码,尤其一些简单的 Android API,那么就需要使用 Mockito 库模拟依赖,辅助单元测试。首先,要添加 Mockito 的依赖(版本号应参考最新版本说明):

1
textCompile 'org.mockito:mockito-core:1.10.19'

而且,需要在包裹测试代码的类生命前标记注解 @RunWith(MockitoJUnitRunner.class),同时对需要模拟的对象标记注解 @Mock。然后用方法 when() 包裹被模拟对象可能调用的方法,后接 thenReturn() 方法传入模拟的返回值。一个简单的示例为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(MockitoJUnitRunner.class)
public class UnitTestSample {

    private static final String FAKE_STRING = "HELLO WORLD";

    @Mock
    Context mMockContext;

    @Test
    public void readStringFromContext_LocalizedString() {
        // Given a mocked Context injected into the object under test...
        when(mMockContext.getString(R.string.hello_word))
                .thenReturn(FAKE_STRING);
        ClassUnderTest myObjectUnderTest = new ClassUnderTest(mMockContext);

        // ...when the string is returned from the object under test...
        String result = myObjectUnderTest.getHelloWorldString();

        // ...then the result should be the expected one.
        assertThat(result, is(FAKE_STRING));
    }
}

如果调用了没有使用 when 方法设置的方法,那么测试就会发生错误。

进阶使用

测试套件的声明非常简单,在需要声明为套件的类声明之前加上注解 @RunWith(Suite.class),以及 @Suite.SuiteClasses(UnitTest1.class, UnitTest2.class),其中 UnitTest1.classUnitTest2.class 代表套件应包含的测试用例的类名。比如:

1
2
3
4
5
6
@RunWith(Suite.class)
@Suite.SuiteClasses({
   TestJunit1.class ,TestJunit2.class
})
public class TestSuite {
}

使用测试套件可以更方便的管理测试用例,而且测试套件可以嵌套测试套件类,实现多层树形结构。

@Before 注解用来标记方法使之在所有测试方法之前执行,一般用来初始化必要的测试环境,比如:注入依赖、开启持久化连接、准备复杂的输入参数等。

@After 用来标明方法使之在所有测试方法执行之后运行,一般用来回收资源和清理测试环境,比如:注销依赖、关闭持久化连接、清空输入和输出等。

@Ignore 标记的测试方法不会被执行,可以用来标记一些待完成或者暂时不适合运行的测试方法。

@Test(timeout=xxx) 可设置为正整数数值,单位为毫秒,测试方法如果超过设置的时长就会失败。

@Test(expected=xxxException.class) 设置为异常类类型,测试代码是否抛出了制定种类的异常。

实现步骤如下:

  • 用 `@RunWith(Parameterized.class)`` 来注释测试类
  • 创建一个由 @Parameterized.Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合。
  • 创建一个公共的构造函数,它接受和一行测试数据相等同的东西
  • 为每一列测试数据创建一个实例变量
  • 用实例变量作为测试数据的来源来创建你的测试用例

比如,对于如下方法进行测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class PrimeNumberChecker {
   public Boolean validate(final Integer primeNumber) {
      for (int i = 2; i < (primeNumber / 2); i++) {
         if (primeNumber % i == 0) {
            return false;
         }
      }
      return true;
   }
}

测试类为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.util.Arrays;
import java.util.Collection;

import org.junit.Test;
import org.junit.Before;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;

@RunWith(Parameterized.class)
public class PrimeNumberCheckerTest {
   private Integer inputNumber;
   private Boolean expectedResult;
   private PrimeNumberChecker primeNumberChecker;

   @Before
   public void initialize() {
      primeNumberChecker = new PrimeNumberChecker();
   }

   // Each parameter should be placed as an argument here
   // Every time runner triggers, it will pass the arguments
   // from parameters we defined in primeNumbers() method
   public PrimeNumberCheckerTest(Integer inputNumber, 
      Boolean expectedResult) {
      this.inputNumber = inputNumber;
      this.expectedResult = expectedResult;
   }

   @Parameterized.Parameters
   public static Collection primeNumbers() {
      return Arrays.asList(new Object[][] {
         { 2, true },
         { 6, false },
         { 19, true },
         { 22, false },
         { 23, true }
      });
   }

   // This test will run 4 times since we have 5 parameters defined
   @Test
   public void testPrimeNumberChecker() {
      System.out.println("Parameterized Number is : " + inputNumber);
      assertEquals(expectedResult, 
      primeNumberChecker.validate(inputNumber));
   }
}

测试的结果为:

1
2
3
4
5
6
Parameterized Number is : 2
Parameterized Number is : 6
Parameterized Number is : 19
Parameterized Number is : 22
Parameterized Number is : 23
true