0%

第9章-单元测试

照例,如果自学不需要看我这个博客的话,资料如下:
官网关于Android Test的介绍的地址
Android官方关于测试的例子 ,需要的自取

单元测试、集成测试、黑盒测试、白盒测试等,只有单元测试是我们开发人员需要自己完成的,其余都是由测试人员完成的。单元测试本质上也是代码,是验证代码正确性的代码。

为什么要做单元测试

  • 便于后期重构。单元测试为后期测试提供了保障,在重构之后,只要单元测试还是全部通过,那么在很大程度上表示重构没有引入新的bug。
  • 优化设计。编写单元测试将使开发者从调用者的角度观察和思考,这样迫使开发者把程序设计成易于调用和低耦合的易测试的形式。
  • 避免代码出现回归。编写完成后,可以随时随地快速运行测试,而不是要求将代码部署到设备上,再手动执行覆盖各种路径。
  • 文档记录。单元测试是极好的“官方文档”,它展示函数或者类如何使用。

Android 测试类型(选自官网)

测试代码的位置取决于您要编写的测试的类型。 Android Studio 为以下两种测试类型提供了源代码目录(源集):

本地单元测试

位于 module-name/src/test/java/目录。

这些测试在计算机的本地 Java 虚拟机 (JVM) 上运行。 当您的测试没有 Android 框架依赖项或当您可以模拟 Android 框架依赖项时,可以利用这些测试来尽量缩短执行时间。

在运行时,这些测试的执行对象是去掉了所有 final 修饰符的修改版 android.jar。 这样一来,您就可以使用 Mockito 之类的常见模拟库。

Instrumented测试(仪器测试)

位于 module-name/src/androidTest/java/。

这些测试在硬件设备或模拟器上运行。 这些测试有权访问 Instrumentation API,让您可以获取某些信息(例如您要测试的应用的 Context), 并且允许您通过测试代码来控制受测应用。 可以在编写集成和功能 UI 测试来自动化用户交互时,或者在测试具有模拟对象无法满足的 Android 依赖项时使用这些测试。

由于仪器测试内置于 APK 中(与您的应用 APK 分离),因此它们必须拥有自己的 AndroidManifest.xml 文件。 不过,由于 Gradle 会自动在构建时生成该文件,因此它在您的项目源集中不可见。 您可以在必要时(例如需要为 minSdkVersion 指定其他值或注册测试专用的运行侦听器时)添加自己的清单文件。 构建应用时,Gradle 会将多个清单文件合并成一个清单。

Gradle 构建解读这些测试源集的方式与其解读项目应用源集的方式相同,您可以利用这一点根据构建变体创建测试。

以下示意图诠释了两种测试的代码结构(图中1表示的是仪器测试的代码,2表示的是单元测试的代码结构)

单元测试与仪器测试示意图

Junit4

在Android测试框架中,常用的有以下几个框架和工具类:JUnit4、AndroidJUnitRunner、Mockito、Espresso,其中主要的单元测试使用Junit4。Junit4是一套基于注解的单元测试框架,在Android studio中,编写在test目录下的测试类都是基于该框架实现,该目录下的代码直接运行在本地的JVM上,不需要Android真机或者模拟器支持。常用的注解如下(更多内容可以查看Junit4官网):

  • @BeforeClass 测试类里所有用例运行之前,运行一次这个方法。方法必须是public static void
  • @AfterClass 与BeforeClass对应
  • @Before 在每个用测试例运行之前都运行一次。
  • @After 与Before对应
  • @Test 指定该方法为测试方法,方法必须是public void
  • @RunWith 测试类名之前,用来确定这个类的测试运行器
    以下用一个简单的测试类来展示测试类的大概形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CaculatUtilTest{
private CaculatUtil mCaculatUtil;

@Before
public void setUp(){
mCaculatUtil = new CaculatUtil();
}

@Test
public void addTwoNumbers(){
assertEquals(3,mCaculatUtil.add(1,2));
//或者如果是静态方法,就类似于以下这种静态调用方法
assertEquals(3,Caculator.add(1,2));
}
}

Junit的断言和失败提示

Junit提供了多个以assert开头的函数,分别用来验证各类相等性质的问题,大致有如下几类:

  • assertEquals

    assertEquals的作用是判断两个值或者对象是否相等。接受2个参数,参数1为预期值,参数2为计算得到的值。

  • assertTrue 与 assertFalse

    assertTrue 与 assertFalse顾名思义就是分别验证真与假,只需要一个boolean类型的参数。例如 assertTrue(false)测试会失败, 而 assertTrue(true) 测试通过。

  • assertNull 与 assertNotNull

    和assertTrue、assertFalse类似,只不过是用来判断空或者非空。例如:assertNull(null) 会测试失败,因为值为null;而assertNull(“hell”)就能测试通过。

  • assertSame 与 assertNotSame

    assertSame用于判断两个对象是否是同一个对象,与assertEquals不同的是,assertSame强调的为同一个对象,而assertEquals只要两个对象相等即可(即调用equals函数时返回true)。

  • failNotEquals

    函数有3个参数,参数1位失败时提示信息,参数2为期望值,参数3是实际值。当两个对象不相等时抛出参数1的错误信息。

  • failSame与failNotSame

    failNotSame与failNotEquals类似,不是同一个对象时就抛出参数1的错误信息。

  • fail(String) 与 fail()

    fail(String)直接抛出当前测试用例参数1中的错误信息,而fail()给出默认的错误信息。

运行多个测试类——TestSuite

如果需要同时运行两个或多个Test类,JUnit提供了Suite注解,在对应的测试目录下创建一个空Test类:

  • @RunWith(Suite.class):配置Runner运行环境。
  • @Suite.SuiteClasses({A.class, B.class}):添加需要一起运行的测试类。
1
2
3
4
5
@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class, CalculatorWithParameterizedTest.class})
public class UnitTestSuite{

}

上述代码中,UnitTestSuite成了一个空类,测试类被添加到注解中了。
或者,如果不用注解,可以通过JUnit4TestAdapter包装测试类,并将JUnit4TestAdapter对象添加到TestSuit中,示例代码如下:

1
2
3
4
5
6
7
8
9
public class MathTestSuite{
public static Test suite(){
TestSuite suite = new TestSuite("com.book.jtm");
//添加测试用例
suite.addTest(new JUnit4TestAdapter(AdderTest.class));
suite.addTest(new JUnit4TestAdapter(DiverTest.class));
return suite;
}
}

上述代码有一个静态的suite函数,它返回一个Test对象,这个对象是TestSuite类型的。测试时,以Junit测试用例的形式运行这个MathTestSuite即可运行这两个测试类。

多个参数输入测试

当需要传入多个参数进行测试时,可以使用 @Parameters 来进行单个方法的多次不同参数的测试,对于测试类,使用该方法需要如下步骤:

  • 在测试类上添加@RunWith(Parameterized.class)注解
  • 添加测试类的构造函数
  • 添加获取参数集合的static方法,并在方法上添加@Parameters注解
  • 在需要测试的方法中直接使用成员变量,该变量由JUnit通过构造方法生成

直接上示例代码如下:

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
@RunWith(Parameterized.class)//为测试类添加注解
public class CaculatUtilTest{
//两个传入的参数
private final int paramOne;
private final int paramTwo;
//期望值
private final int expectResult;

private CaculatUtil mCaculatUtil;

public CaculatUtilTest(int paramOne,int paramTwo,int expectResult){//添加构造函数
this.paramOne = paramOne;
this.paramTwo = paramTwo;
this.expectResult = expectResult;
}

//添加获取参数集合的static方法,并在方法上添加@Parameters注解
@Parameters
public static Collection<Object[]> initTestData(){
return Arrays.asList(new Object[][]{
{0,0,0},
{1,1,2},
{1,5,6}
});
}

@Before
public void setUp(){
mCaculatUtil = new CaculatUtil();
}

@Test
public void addTwoNumbers(){
//测试的方法中直接使用成员变量
assertEquals(expectResult,mCaculatUtil.add(paramOne,paramTwo));
}
}

AndroidJUnitRunner

当单元测试中涉及Android系统库的调用时,可以通过AndroidJUnitRunner方案完成测试,这样就能在测试类中使用Context、parcelable、Shareprefrence等类。使用方法是在androidTest目录下创建测试类(因为这涉及到Instrumented测试的内容),在该类上添加@RunWith(AndroidJUnit4.class)注解。如以下代码示范了如何在测试类中使用SharedPrefrences:

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
50
51
52
53
54
55
@RunWith(AndroidJUnit4.class)
public class SharedPreferencesHelperTest {

private static final String TEST_NAME = "Test name";

private static final String TEST_EMAIL = "test@email.com";

private static final Calendar TEST_DATE_OF_BIRTH = Calendar.getInstance();

private SharedPreferenceEntry mSharedPreferenceEntry;

private SharedPreferencesHelper mSharedPreferencesHelper;

private SharedPreferences mSharePreferences;

/** 上下文 */
private Context mContext;
……
@Before
public void setUp() throws Exception {
//获取application的context
mContext = InstrumentationRegistry.getTargetContext();
//实例化SharedPreferences
mSharePreferences = PreferenceManager.getDefaultSharedPreferences(mContext);

mSharedPreferenceEntry = new SharedPreferenceEntry(TEST_NAME, TEST_DATE_OF_BIRTH, TEST_EMAIL);
//实例化SharedPreferencesHelper,依赖注入SharePreferences
mSharedPreferencesHelper = new SharedPreferencesHelper(mSharePreferences);

//以下是在mock的相关操作,模拟commit失败
mMockSharePreferences = Mockito.mock(SharedPreferences.class);
mMockBrokenEditor = Mockito.mock(SharedPreferences.Editor.class);
when(mMockSharePreferences.edit()).thenReturn(mMockBrokenEditor);
when(mMockBrokenEditor.commit()).thenReturn(false);
mMockSharedPreferencesHelper = new SharedPreferencesHelper(mMockSharePreferences);
}

/**
* 测试保存数据是否成功
*/
@Test
public void sharedPreferencesHelper_SavePersonalInformation() throws Exception {
assertThat(mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry), is(true));
}
/**
* 测试保存数据,然后获取数据是否成功
*/
@Test
public void sharedPreferencesHelper_SaveAndReadPersonalInformation() throws Exception {
mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry);
SharedPreferenceEntry sharedPreferenceEntry = mSharedPreferencesHelper.getPersonalInfo();
assertThat(isEquals(mSharedPreferenceEntry, sharedPreferenceEntry), is(true));
}
……
}

模拟所需要的模块

有时我们测试需要依赖于其他的功能模块,但是某些原因这个功能模块不能在测试时运用或未开发完,为了不阻塞测试,我们可以Mock对象来完成测试。还有一些场景,诸如对象很难被创建、真实对象运行缓慢、真实对象的错误很难出现等,也可以通过Mock对象来测试。

手动Mock对象

举个例子,开发一款记事本软件,登录成功后才能写/存笔记,小明小刘分别负责登录和写/存笔记功能,存笔记的时候时候需要用户信息User的实例,而用户信息在登录成功后才能获得。可行的代码如下:

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
//保存数据的类
public class NoteDAO{
private NoteDAO noteDAO;

public void saveNote(User user,String note){
Log.d("NoteDAO","存储笔记");
}
}

//测试类
public class NoteTest{
@Before
public void setUp(){
noteDAO = new NoteDAO();
}

@Test
public void testSaveNote(){
MockLoginImpl loginImpl = new MockLoginImpl();
noteDAO.saveNote(loginImpl.login("dd","pwd"),"note_content");
}
}


//Mock类
public class MockLoginImpl {
public User login(String name,String pwd){
return new User(name,"1234556");
}
}

使用第三方工具Mockito

前面有例子已经涉及到Mockito的部分使用,可以在网上搜索相关使用,这里不再详细展开,如果需要,后面会专门介绍。

运行单元测试

  • 在Android studio中,对指定的测试类点击鼠标右键,选择对应的Run或者debug
  • 在Terminal输入gradle testDebugUnitTest或gradle testReleaseUnitTest指令来分别运行debug和release版本的unittesting,在执行的结果可以在xxx\project\app\build\reports\tests\testReleaseUnitTest中查看

声明:整篇文章有部分内容摘抄自博客:https://www.jianshu.com/p/925191464389

谢谢你的鼓励