湖畔镇

Android——测试

本文介绍了Android应用的常用测试类型

单元测试

单元测试是编写测试代码,用来检测特定的、明确的、细颗粒的功能

单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构之后的正确性

一般来说,单元测试任务包括

  1. 接口功能测试:用来保证接口功能的正确性
  2. 局部数据结构测试(不常用):用来保证接口中的数据结构是正确的

    1. 比如变量有无初始值
    2. 变量是否溢出
  3. 边界条件测试

    1. 变量没有赋值(即为NULL)
    2. 变量是数值(或字符)

      1. 主要边界:最小值,最大值,无穷大(对于DOUBLE等)
      2. 溢出边界(期望异常或拒绝服务):最小值-1,最大值+1
      3. 临近边界:最小值+1,最大值-1
    3. 变量是字符串

      1. 引用“字符变量”的边界
      2. 空字符串
      3. 对字符串长度应用“数值变量”的边界
    4. 变量是集合

      1. 空集合
      2. 对集合的大小应用“数值变量”的边界
      3. 调整次序:升序、降序
    5. 变量有规律:比如对于Math.sqrt,给出n^2^-1和n^2^+1的边界

  4. 所有独立执行通路测试:保证每一条代码,每个分支都经过测试,AndroidStudio中集成了Jacoco可以做覆盖率统计

    1. 语句覆盖:保证每一个语句都执行到了
    2. 判定覆盖(分支覆盖):保证每一个分支都执行到
    3. 条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
    4. 路径覆盖:保证每一个路径都覆盖到
    5. 各条错误处理通路测试:保证每一个异常都经过测试

JUnit

JUnit4通过注解来识别测试方法

  • @BeforeClass 全局执行一次,第一个运行
  • @Before 测试方法运行前运行
  • @Test 测试方法
  • @After 测试方法运行后运行
  • @AfterClass 全局执行一次,最后一个运行
  • @Ignore 忽略

Mockito

Mock测试是单元测试的重要方法之一,就是对某些不容易构造或难以获取的对象,用一个虚拟的Mock对象创建以便测试的方法

最大的有点是可以解除单元测试的耦合,如果你的代码对另一个类或接口由依赖,能够模拟这些依赖,帮你验证调用的依赖行为

使用一个接口来描述这个对象 在产品代码中实现这个接口,在测试代码中实现这个接口 在被测试代码中只是通过接口来引用对象,所以它不知道这个引用的对象是真实对象,还是Mock对象

Mockito是很强大的单元测试Mock框架

验证行为

1
2
3
4
5
6
7
8
9
10
11
12
import static org.mockito.Mockito.*;

//mock creation
List mockedList = mock(List.class);

//using mock object
mockedList.add("one");
mockedList.clear();

//verification
verify(mockedList).add("one");
verify(mockedList).clear();

它会记得所有的交互,你可以验证

打桩

1
2
3
4
5
6
7
LinkedList mockedList = mock(LinkedList.class);
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
System.out.println(mockedList.get(0));
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(999));
verify(mockedList).get(0);

可以为模拟的接口打桩,指定其行为,上面取第一个元素就会返回first,取第二个元素会抛出异常,取没有打桩的元素会返回null

参数匹配器

1
2
3
4
when(mockedList.get(anyInt())).thenReturn("element");
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
System.out.println(mockedList.get(999));
verify(mockedList).get(anyInt());

anyInt()匹配所有元素,用argThat()指定自定义参数匹配

1
2
3
4
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));

// 错误,所有的参数都应该由匹配器提供
verify(mock).someMethod(anyInt(), anyString(), "third argument");

调用额外的调用数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
verify(mockedList, never()).add("never happened");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");

处理异常

1
2
doThrow(new RuntimeException()).when(mockedList).clear();
mockedList.clear();

有序验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List singleMock = mock(List.class);
singleMock.add("was added first");
singleMock.add("was added second");

InOrder inOrder = inOrder(singleMock);

inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");

List firstMock = mock(List.class);
List secondMock = mock(List.class);

firstMock.add("was called first");
secondMock.add("was called second");

InOrder inOrder = inOrder(firstMock, secondMock);

inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");

确保不会发生交互

1
2
3
4
mockOne.add("one");
verify(mockOne).add("one");
verify(mockOne, never()).add("two");
verifyZeroInteractions(mockTwo, mockThree);

寻找多余调用

1
2
3
4
5
6
mockedList.add("one");
mockedList.add("two");
verify(mockedList).add("one");

// 将会失败
verifyNoMoreInteractions(mockedList);

一个列子

Person.java

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
public class Person {
private Integer id;
private String name;

public Person(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

PersonDAO.java

1
2
3
4
public interface PersonDAO {
Person fetchPerson(Integer id);
void update(Person person);
}

PersonService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PersonService {
private PersonDAO personDAO;

public PersonService(PersonDAO personDAO) {
this.personDAO = personDAO;
}

public boolean update(Integer id, String name) {
Person person = personDAO.fetchPerson(id);
if (person != null) {
Person updatedPerson = new Person(person.getId(), name);
personDAO.update(updatedPerson);
return true;
} else {
return false;
}
}
}

PersonServiceTest.java

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
public class PersonServiceTest {
@Mock private PersonDAO personDAO;
private PersonService personService;

@Before
public void setUp() throws Exception {
// 初始化
MockitoAnnotations.initMocks(this);
personService = new PersonService(personDAO);
}

@Test
public void shouldUpdatePersonName() throws Exception {
Person person = new Person(1, "Phillip");
// 传入1时返回菲利普
when(personDAO.fetchPerson(1)).thenReturn(person);
// 测试update
boolean updated = personService.update(1, "David");
// 判断是否更新成功(应该成功)
assertTrue(updated);

// 验证是否调用fetchPerson
verify(personDAO).fetchPerson(1);
// 验证是否调用update
ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass(Person.class);
verify(personDAO).update(personCaptor.capture());
Person updatedPerson = personCaptor.getValue();
// 判断更新后的返回值
assertEquals("David", updatedPerson.getName());
// 判断是否有更多交互
verifyNoMoreInteractions(personDAO);
}

@Test
public void shouldNotUpdateIfPersonNotFound() {
// 传入1时返回null
when(personDAO.fetchPerson(1)).thenReturn(null);
boolean updated = personService.update(1, "David");
// 判断是否更新成功(应该失败)
assertFalse(updated);
// 验证是否调用fetchPerson
verify(personDAO).fetchPerson(1);
verifyZeroInteractions(personDAO);
verifyNoMoreInteractions(personDAO);
}
}

仪器测试

Espresso

测试登录页面跳转首页,判断是否登录成功

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
@RunWith(AndroidJUnit4.class)
@LargeTest
public class LoginTest {

@Rule
public ActivityTestRule<LoginActivity> mActivityRule = new ActivityTestRule<>(LoginActivity.class);


@Test
public void loginWithWrongPassword() {
onView(withId(R.id.et_username)).perform(replaceText("android"));
closeSoftKeyboard();
onView(withId(R.id.et_password)).perform(replaceText("wrong"));
closeSoftKeyboard();
onView(withId(R.id.btn_login)).perform(click());
onView(withId(R.id.tv_result)).check(matches(withText("登录失败")));
}


@Test
public void loginWithRightPassword() {
onView(withId(R.id.et_username)).perform(replaceText("android"));
closeSoftKeyboard();
onView(withId(R.id.et_password)).perform(replaceText("123456"));
closeSoftKeyboard();
onView(withId(R.id.btn_login)).perform(click());
onView(withId(R.id.tv_result)).check(matches(withText("登录成功")));
}
}

不是原生输入法,typeText()可能导致输入不完整而测试失败,使用replaceText()立刻填充或换回原生输入法

更多用法

压力测试

Monkey

分享