Android Unit Test 方案小结:JUnit & Mockito & PowerMock

文章目录
  1. 1. 简介单元测试
  2. 2. 说说 JUnit
  3. 3. 说说 Mockito
  4. 4. 说说 PowerMock
  5. 5. 参考文献

天之道,损有余而补不足,是故虚胜实,不足胜有余。

从小厂来现在的厂已有半年了,从完全不知道单元测试为何物到断断续续地写了不少单元测试,期间有过痛苦与挣扎,所幸有问题时,咨询大佬能顺利地解决。心怀感激,回首过往,满满的都是成长。这一期,就对单元测试方案来作归纳与小结。

简介单元测试

单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括父类、子类和抽象类的方法。

以上节选自维基百科。

更详细点说,单元测试就是开发工程师在项目中为了测试某个代码单元而写的测试代码,用来执行项目中的目标函数,检测目标代码的准确性与可靠性,并验证其逻辑状态或结果。

或许有些人会走入误区,特别是对于我们移动端开发者,可能会觉得,效果做出来就行了,写什么单元测试,徒增工作量。当初我也是这么想的,然而写了些单元测试后,确实觉得对自己写的东西更有信心了,也提高了代码的健壮性(我会说是老大要求的吗,开个玩笑。实际上,有点代码追求或者稍微有点规模的厂,对单元测试还是有要求的,这是软件开发过程中重要的一环)。好,大概总结下 Android 开发中写单元测试的好处:

  • 适应变更

若项目有多人经手,有时候想重构部分代码,又担心重构后出现问题,那么可以边重构边写单元测试,至少可以保证重构过程中,没有破坏原有代码逻辑的正确性。良好设计的单元测试案例,会覆盖程序单元分支和循环条件的所有路径。

  • 简化集成

做好单元测试,后期的系统集成联调或集成测试和系统测试会很顺利,节约时间。同时,单元测试过程中能发现一些深层次的问题,也会发现一些容易察觉而在集成测试和系统测试时很难发现的问题。

  • 文档记录

单元测试提供了系统的一种文档记录。通过查看单元测试提供的功能,和单元测试中如何使用程序单元,开发人员能直观地理解该单元的基础 API。

  • 表达设计

测试驱动开发的软件实践中,单元测试可以取代正式的设计。每个单元测试案例都可以视为一项类、方法和待观察行为等设计元素。单元测试本身可以用于验证程序实现匹配设计。

不过,也要注意单元测试的局限性

单元测试只测试程序单元自身的功能,不能发现集成错误、性能问题或者其他系统级别的问题。

有些问题不是简单地检测出来,比如具有不确定性或牵扯到多线程的问题。

替单元测试写的代码可能就像要测试的代码一样有程序错误,等等。

总之,为了实现单元测试的益处,在软件开发过程中应形成一套严格的纪律意识。需要保留好记录,不但保留执行的测试,也要保留对应的源码和其他软件单元的变更历史。

说说 JUnit

JUnit 是一个 Java 语言的单元测试框架,多数 Java 开发环境中集成了 JUnit 作为单元测试的工具,如 Android Studio 创建的工程中默认在 dependencies 中集成了 JUnit4,如下:

1
2
3
dependencies {
testCompile "junit:junit:4.12"
}

写一个简单的 Demo,比如定义这样一个类如下:

1
2
3
4
5
6
7
public class Calculator {

public int add(int one, int another) {
return one + another;
}

}

所定义的一个 CalculatorTest 类如下:

1
2
3
4
5
6
7
8
9
10
public class CalculatorTest {

@Test
public void testAdd() {
Calculator calculator = new Calculator();
int sum = calculator.add(1, 2);
assertEquals(3, sum);
}

}

以上,CalculatorTest 是 Calculator 对应的测试类,testAdd() 是 add() 对应的测试方法。故在单元测试时,就是给目标类的 public 方法写对应的测试方法。一般来说,一个方法对应的测试方法主要有三步,以 CalculatorTest 为例:

1
2
3
I. 建立环境,一般是 new 出要测试的目标类,以及设立一些前提条件,如 Calculator calculator = new Calcultor();
II. 执行操作,一般是调用所要测试的方法,并获得结果,如 int sum = calculator.add(1, 2);
III. 验证结果,通过 JUnit4 提供的 Assert 来验证结果是否与期待的相同,如 Assert.assertEquals(3, sum);

上面的例子中,有个@Test注解,通过声明注解,告诉 JUnit 要测的方法,以下是 JUnit4 常用的几个注解:

@Before:初始化方法,对每一个测试方法都要执行一次;

@After:释放资源,对每一个测试方法都要执行一次;

@Test:测试方法,测试期望异常和超时时间;

@Ignore:忽略的测试方法;

@BeforeClass:对于所有测试方法只执行一次,且必须为 static void。

@AfterClass:对于所有测试方法只执行一次,且必须为 static void。

注意,JUnit4 的单元测试执行顺序为:

@BeforeClass -> @Before -> @Test -> @After -> @AfterClass;

其中,每个测试方法的调用顺序为:

@Before -> @Test -> @After。

更多精彩参见 JUnit 官网

说说 Mockito

写单元测试时,一些情况下,要测试的目标类会有很多依赖,其依赖的类/对象/资源又会有别的依赖,甚至形成一个大的依赖树,在单元测试的环境中完整地构建依赖有几分困难。Mock 便可以顺利地解决这些问题,即对测试的类所依赖的其他类和对象进行 Mock,构建一个假的对象,定义假对象上的行为,提供给被测试对象使用。使用这种方式,可以把测试的目标限定于被测试对象的本身,形成一个尽量小的被测试目标。

所谓的 Mock,就用到了即将谈到的 Mockito 测试框架了,其广泛应用于 Java 程序的单元测试中。测试环境中,通过 Mockito 以 Mock 出其他的依赖对象,用来替换真实的对象,让待测的目标方法被隔离起来,避免一些外界因素的影响和依赖,能在预设的环境中执行。Mockito 的使用如下:

首先在 Gradle 中配置:

1
2
3
4
5
6
7
8
repositories { 
jcenter()
}

dependencies {
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.10.19"
}

示例的 Demo 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ListTest {

@Test
public void testGet() {
// 创建 Mock 对象
List list = Mockito.mock(List.class);
// 设置 Mock 对象的行为,设定调用其 get 方法获取第 0 个元素时,返回 "one"
Mockito.when(list.get(0)).thenReturn("one");
// 使用 Mock 对象,即使列表为空,也会返回上面设置好的值 "one"
String str = (String) list.get(0);

Assert.assertTrue(list.size() == 0);
Assert.assertTrue("one".equals(str));
// 验证 Mock 对象的 get 方法被调用过,且调用时的传参是 0
Mockito.verify(list).get(0);
}

}

代码逻辑如上,可以了解到在 Mockito 框架中:

1
2
3
I. 通过 Mockito.mock() 方法来 Mock 出对象,该对象可以是目标类的外界依赖对象,如 List list = Mockito.mock(List.class);
II. 通过 Mockito.when().thenReturn() 方法为某个 Mock 对象的方法指定返回值,以执行特定的动作,如 Mockito.when(list.get(0)).thenReturn("one");
III. 通过 Mockito.verify().doSomething(matchParam) 方法来验证目标方法的调用情况(如调用次数和调用参数等),如 Mockito.verify(list).get(0);

注意

  • Mockito.mock() 并不是 Mock 出一整个类,却是根据传进去的一个类,Mock 出属于该类的一个对象,并且返回该 Mock 对象。同时,传进去的类本身没有改变,该类 new 出的对象也没有任何改变。
  • Mockito.verify() 其括号中的参数必须是 Mock 对象,Mockito 只验证 Mock 对象的调用情况。
  • Mockito.mock() 出来的对象不会自动替换掉正式代码里的对象,必须应用某些方式(如依赖注入、构造方法注入和 set 方式注入等)将 Mock 对象应用到正式代码里。
  • Mockito.spy() 默认会调用该类的真实的方法,并返回相应的返回值,也可以通过 Mockito.when().thenReturn() 来指定 spy 对象方法的行为。

对 spy 对象的方法定制示例 Demo 如下:

1
2
3
4
5
6
7
8
9
10
@Test
public void testSpy() {
List list = new LinkedList();
List spy = Mockito.spy(list);

// 会调用真实的方法,list 为空,故会抛出 IndexOutOfBoundsException
Mockito.when(spy.get(0)).thenReturn("foo");
// 使用 doReturn() 可行
Mockito.doReturn("foo").when(spy).get(0);
}

另外需要注意的是,Mockito.mock() 出来的对象,未指定情况下,Mock 对象的所有非 void 方法将返回默认值:int、long 类型方法将返回 0,boolean 方法将返回 false,对象方法将返回 null 等;而 void 方法什么都不会做。

如:

1
2
List list = Mockito.mock(List.class);
System.out.println(list.get(1));

由于未指定 list.get(1) 的返回值,默认就会返回 null。

更进一步,上文提到的 JUnit 中,可以通过 @Before、@Test、@After 等注解表示测试方法。Mockito 中,同样支持对变量注解,如将 Mock 对象设为测试类的属性,通过 @Mock 定义。常见的还有监视真实的对象 @Spy、参数捕获器 @Captor 和 Mock 对象自动注入 @InjectMocks。

首先,要做好注解的初始化

只有注解不够,要首先初始化,初始化方法有两种:

其一,MockitoAnnotations.initMocks(testClass),参数 testClass 即为所写的测试类,在 JUnit 的 @Before 方法中执行初始化操作:

1
2
3
4
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}

其二,使用 Mockito 提供的 MockitoJUnitRunner:

1
2
3
4
@RunWith(MockitoJUnitRunner.class)
public class Test {
// ...
}

下面,分别来看看几个注解:

@Mock 注解

其优点如下:

  • 方便 Mock 对象的创建;
  • 减少 Mock 对象创建的重复代码;
  • 提高测试代码的可读性;
  • 变量名字作为 Mock 对象的标识,易于排错。
1
2
3
4
5
6
7
@RunWith(MockitoJUnitRunner.class)
public class Test {

@Mock
private List mList;

}

@Spy 注解

使用 @Spy 生成的类,其方法为真实方法,返回值和真实方法一样。

1
2
3
4
5
6
7
@RunWith(MockitoJUnitRunner.class)
public class Test {

@Spy
List list = new LinkedList();

}

@Captor 注解

@Captor 是参数捕获器的注解,通过注解的方式更便捷地对 ArgumentCaptor 进行定义。通过 ArgumentCaptor 对象的 forClass(Class clazz) 方法构建 ArgumentCaptor 对象,便可以在验证时对方法的参数进行捕获,最后验证捕获的参数值。若方法有多个参数需要捕获验证,则需要创建多个 ArgumentCaptor 对象处理。其中:

1
2
3
argument.capture() 捕获方法参数;
argument.getValue() 获取方法参数值,若方法进行多次调用,则返回最后一个参数值;
argument.getAllValues() 进行多次调用后,返回多个参数值。

示例 Demo 如下:

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

@Mock(name = "mManager")
private Manager mManager;

private Presenter mPresenter;

@Captor
private ArgumentCaptor<Callback<Config>> mCaptor;

@Before
public void setUp() {
mPresenter = new Presenter(mManager);
}

@Test
public void testGetReasons() {
mPresenter.getReasons();
Mockito.verify(mManager).getReasons(captor.capture());
Assert.assertNotNull(captor.getValue());
}

}

验证 mManager 是否调用 getReasons() 方法,是否传入 Callback 参数。实际上,通过 ArgumentCaptor 可以对异步方法进行测试。

@InjectMocks 注解

通过该注解,实现自动注入 Mock 对象。Mockito 首先尝试类型注入,若有多个类型相同的 Mock 对象,则根据名称进行注入。注意,注入失败的时候,Mockito 不会抛出任何异常,需要手动去验证其安全性。

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
@RunWith(MockitoJUnitRunner.class)
public class ListTest {

@Mock(name = "mManager")
private Manager mManager;

@InjectMocks
private Presenter mPresenter;

@Captor
private ArgumentCaptor<Callback<Config>> mCaptor;

@Before
public void setUp() {
mPresenter = new Presenter(mManager);
}

@Test
public void testGetReasons() {
mPresenter.getReasons();
Mockito.verify(mManager).getReasons(captor.capture());
Assert.assertNotNull(captor.getValue());
}

}

考虑这种情况,很多时候不关心被调用方法的具体参数,或者只关心方法得到调用。Mockito 中提供了一系列 any 方法,来表示任何参数都是可以的,如Mockito.verify(mManager, times(1)).getReasons(any(Callback.class))。any(Callback.class) 表示任何一个 Callback 对象都可以,甚至 null 也可以。

再深入一步,Mockito 对象这件事,其本质是代理模式的应用。代理模式说的即是,在一个真实对象前,提供一个代理对象,所有对真实对象的调用,都先经过代理对象。然后,由代理对象根据情况,决定相应的处理,可以直接做一个自己的处理,也可以调用真实对象对应的方法。代理对象对调用者来说,可以是透明的,也可以是不透明的。此外,Mockito 使用了 CGLIB,其是一个强大的高性能代码生成包,许多 AOP 框架在用。

更多精彩参见 Mockito 官网

说说 PowerMock

由于 Mockito 生成 Mock 对象的原理基于 CGLIB,而 CGLIB 生成代理对象有其局限性,如 final 类型、private 类型及静态类型的方法无法 Mock。PowerMock 便完美地弥补其不足。

PowerMock 是一个扩展了如 EasyMock 等 Mock 框架的、功能更加强大的测试框架。PowerMock 使用一个自定义类加载器和字节码操作来模拟静态方法、构造函数、final 类和方法、私有方法和去除静态初始化器等。

同样,首先在 Gradle 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
repositories { 
jcenter()
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])

testCompile "junit:junit:4.12"

// PowerMock brings in the mockito dependency
testCompile 'org.powermock:powermock-module-junit4:1.6.5'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.5'
testCompile 'org.powermock:powermock-api-mockito:1.6.5'
testCompile 'org.powermock:powermock-classloading-xstream:1.6.5'
}

其有 3 个重要的注解:

1
2
3
@RunWith(PowerMockRunner.class)
@PrepareForTest({YourClassWithEgStaticMethod.class})
@PowerMockIgnore("javax.management.*")

测试用例中使用注解 @PrepareForTest 的话,需要加注解 @RunWith(PowerMockRunner.class),类似 Mockito 的使用。其中,@PrepareForTest 注解是声明需要进行 Mock 的静态类。若需要声明多个静态类,则使用@PrepareForTest({Example1.class, Example2.class, ...})该方式声明。最后,就是 @PowerMockIgnore 注解,声明 package 路径,表示不使用 PowerMock 加载所声明的 package 路径的类。

好,接下来看看 PowerMock 的简单使用:

I. 类似 Mockito 的普通 Mock,Mock 参数传递对象

Demo 代码:

1
2
3
4
5
6
7
public class Example {

public boolean callInstance(File file) {
return file.exists();
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
public class ExampleTest {

@Test
public void testCallInstance() {
File file = PowerMockito.mock(File.class);
Example example = new Example();
PowerMockito.when(file.exists()).thenReturn(true);
Assert.assertTrue(example.callInstance(file));
}

}

即普通 Mock 不需要加 @RunWith 和 @PrepareForTest 注解,此类情况下,Mockito 同样可以实现。

II. Mock 示例方法内部 new 出来的对象

Demo 代码:

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

public boolean callInstance(String path) {
File file = new File(path);
return file.exists();
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ExampleTest {

@Test
@PrepareForTest(Example.class)
public void testCallInstance() {
File file = PowerMockito.mock(File.class);
Example example = new Example();

PowerMockito.whenNew(File.class).withArguments("abc").thenReturn(file);
PowerMockito.when(file.exists()).thenReturn(true);

Assert.assertTrue(example.callArgumentInstance("abc"));

File newFile = Mockito.mock(File.class);
newFile.exists();
Mockito.verify(newFile).exists();
}

}

使用PowerMockito.whenNew().thenReturn()方法时,必须加上注解 @PrepareForTest 和 @RunWith,注解 @PrepareForTest 里写的类即为需要 Mock 的 new 对象代码所在的类。其中,需要 Mock 的对象是在方法内部 new 出来的。注意两点:

1.通过PowerMockito.whenNew(File.class).withArguments("abc").thenReturn(file)指定当以参数 “abc” 创建 File 对象的时候,返回已 Mock 出的 File 对象。

2.测试方法上加的注解@PrepareForTest(Example.class),注解里写的类是需要 Mock 的 new 对象代码所在的类。

III. Mock 普通对象的 final 方法

Demo 代码:

1
2
3
4
5
6
7
public class Example {

public final boolean isAlive() {
return false;
}

}
1
2
3
4
5
6
7
public class Scenario {

public boolean callFinal(Example example) {
return example.isAlive();
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
public class ExampleTest {

@Test
@PrepareForTest(Example.class)
public void testCallFinal() {
Example example = PowerMockito.mock(Example.class);
Scenario scenario = new Scenario();
PowerMockito.when(example.isAlive()).thenReturn(true);
Assert.assertTrue(scenario.callFinal(example));
}

}

注解里写的类是需要 Mock 的 final 方法所在的类。

IV. Mock 普通类的静态方法

Demo 代码:

1
2
3
4
5
6
7
public final class Utils {

public static String generateId() {
return ID.randomID().toString();
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
@PrepareForTest(Utils.class)
public class ExampleTest {

@Test
public void testGenerateId() {
PowerMockito.mockStatic(Utils.class);
PowerMockito.when(Utils.generateId()).thenReturn("Useful ID");
Example example = new Example();
assertThat(example.generateId()).isEqualTo("Useful ID");
}

}

注解 @PrepareForTest 里写的类是静态方法所在的类。

V. Mock 私有方法

Demo 代码:

1
2
3
4
5
6
7
8
9
10
11
public class Example {

public boolean callPrivate() {
return isExisted();
}

private boolean isExisted() {
return false;
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
public class ExampleTest {

@Test
@PrepareForTest(Example.class)
public void testCallPrivate() {
Example example = PowerMockito.mock(Example.class);
PowerMockito.when(example.callPrivate()).thenCallRealMethod();
PowerMockito.when(example, "isExisted").thenReturn(true);
Assert.assertTrue(example.callPrivate());
}

}

注解 @PrepareForTest 里写的类是私有方法所在的类。

VI. Mock 系统类的静态和 final 方法

Demo 代码:

1
2
3
4
5
6
7
public class Example {

public String callSystemStatic(String str) {
return System.getProperty(str);
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ExampleTest {

@Test
@PrepareForTest(Example.class)
public void testCallSystemStatic() {
Example example = new Example();
PowerMockito.mockStatic(System.class);

PowerMockito.when(System.getProperty("abc")).thenReturn("ABC");
Assert.assertEquals("ABC", example.callSystemStatic("abc"));
}

}

注意,此处 @PrepareForTest 里写的类与前几种情况有区别,注解里写的类是需要调用系统方法所在的类

VII. Mock 普通类的私有变量

Demo 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Example {

private static final int READY_NOT = 0;
private static final int READY = 1;

private int mState = READY_NOT;

public boolean isDoAction() {
if (mState == READY) {
return true;
} else {
return false;
}
}

}

测试用例:

1
2
3
4
5
6
7
8
9
10
public class ExampleTest {

@Test
public void testIsDoAction() {
Example example = new Example();
Whitebox.setInternalState(example, "mState", 1);
assertThat(example.isDoAction()).isTrue();
}

}

当需要 Mock 私有变量 mState 时,无需加注解 @PrepareForTest 和 @RunWith,而是使用 WhiteBox 来 Mock 私有变量 mState 并注入预设的变量值。

更多精彩参见 PowerMock 官方 Wiki

注意,PowerMockito 和 Mockito 两种框架 Mock 出的对象不能相互使用,否则会抛出异常。

综上,JUnit & Mockito & PowerMock 三者结合起来,可以很好地解决 Java 的单元测试问题。由于 PowerMock 对 Mockito 有较强依赖,建议配置 dependencies 为:

1
2
3
4
5
6
7
testCompile 'junit:junit:4.11'
// Mockito for unit tests
testCompile 'org.mockito:mockito-core:1.9.5'
// Powermock for unit tests
testCompile 'org.powermock:powermock-module-junit4:1.5.6'
testCompile 'org.powermock:powermock-module-junit4-rule:1.5.6'
testCompile 'org.powermock:powermock-api-mockito:1.5.6'

至此,关于 Android Unit Test 方案小结到此结束,JUnit & Mockito & PowerMock 即为最佳实践。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.http://www.jianshu.com/p/6631bd826677

2.http://www.snowdream.tech/2016/08/03/android-mock-test/