0%

安卓测试-JUnit框架

JUnit是java开发的一个测试框架,Android当然也能用
我个人的感觉是它比较适合测试纯Java逻辑的代码,比如工具类,算法的计算,类的数据操作等等。在app -> src目录下,test就是它的工作目录。 在类名字上右键,go-to,选择test,就可以自动创建一个对应的测试类。在左边的文件目录里面对测试类文件右键,run,就可以执行里面的测试方法。

基础

引入方法:

1
testImplementation 'junit:junit:4.12'

JUnit4基础方法注解和常用的assertEquals之类的断言就不用说了,看名字基本都明白。

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
public class DateUtilTest {

private String time = "2017-10-15 16:00:02";

private long timeStamp = 1508054402000L;

private Date mDate;

@Before
public void setUp() throws Exception {
System.out.println("测试开始!");
mDate = new Date();
mDate.setTime(timeStamp);
}

@After
public void tearDown() throws Exception {
System.out.println("测试结束!");
}

//该方法默认会在主线程中执行
@Test
public void dateToStampTest() throws Exception {
assertNotEquals(4, DateUtil.dateToStamp(time));
}

@Test(expected = ParseException.class)
public void dateToStampTest1() throws Exception {
DateUtil.dateToStamp("2017-10-15");
}

@Test
@Ignore("test方法不执行\n")
public void test() {
System.out.println("-----");
}

//该方法会在一个单独的线程中执行,单位为毫秒,这里超时时间为2秒
@Test(timeout = 2000)
public void testTimeout() {
System.out.println("timeout method called in thread " + Thread.currentThread().getName());
}

@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
new ArrayList<Object>().get(0);
}
}

不过这里的@Test注解还有两个属性可以指定

Hamcrest与assertThat

Hamcrest是一个表达式类库,它提供了一套匹配符Matcher,JUnit4结合Hamcrest提供了一个全新的断言语法:assertThat,结合Hamcrest提供的匹配符,可以表达全部的测试思想。使用gradle引入JUnit4.12时已经包含了hamcrest-core.jar、hamcrest-library.jar、hamcrest-integration.jar这三个jar包,所以我们无需额外再单独导入hamcrest相关类库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//注意由于没有导包,直接用is(),both等等会找不到方法,他们基本都是CoreMatchers类的方法
public class AssertThatTest {

@Test
public void testMobilePhone() throws Exception {
Assert.assertThat("13588888888", new IsMobilePhoneMatcher());
}

@Test
public void testAssertThat1() throws Exception {
Assert.assertThat(6, CoreMatchers.is(6));
}

@Test
public void testAssertThat2() throws Exception {
Assert.assertThat(null, IsNull.nullValue());
}

@Test
public void testAssertThat3() throws Exception {
Assert.assertThat("Hello python world",CoreMatchers.both(CoreMatchers.startsWith("Hello")).and(CoreMatchers.endsWith("World")));
}
}

自定义匹配器

assertThat会用到匹配器,我们也可以自己定义匹配规则

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
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class IsMobilePhoneMatcher extends BaseMatcher<String> {

/**
* 进行断言判定,返回true则断言成功,否则断言失败
*/
@Override
public boolean matches(Object item) {
if (item == null) {
return false;
}
Pattern pattern = Pattern.compile("(1|861)(3|5|7|8)\\d{9}$*");
Matcher matcher = pattern.matcher((String) item);
return matcher.find();
}

/**
* 给期待断言成功的对象增加描述
*/
@Override
public void describeTo(Description description) {
description.appendText("预计此字符串是手机号码!");
}

/**
* 给断言失败的对象增加描述
*/
@Override
public void describeMismatch(Object item, Description description) {
description.appendText(item.toString() + "不是手机号码!");
}
}

自定义 Rule

Rule给我的感觉类似于动态代理里面的InvocationHandler,在测试类中使用 @Rule 注解标记一个 Rule 接口的实现类,那么在 Rule 的 apply方法中就可以拦截到这个测试类的所有测试方法。

自带的Rule示例:

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
//在测试方法内部能知道当前的方法名。
public class NameRuleTest { //用@Rule注解来标记一个TestRule,注意必须是public修饰的
@Rule
public final TestName name = new TestName();

@Test
public void testA() {
assertEquals("testA", name.getMethodName());
}

@Test
public void testB() {
assertEquals("testB", name.getMethodName());
}
}

//超时时间
public class TimeoutRuleTest {
@Rule
public final Timeout globalTimeout = Timeout.millis(20);

@Test
public void testInfiniteLoop1() {
for (; ; ) {
}
}

@Test
public void testInfiniteLoop2() {
for (; ; ) {
}
}
}

自定义Rule:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
* 自定义@RuLL MyRule 演示
*/
public class AssertThatTest {

@Rule
public MyRule rule = new MyRule();

@Test
public void testAssertThat1() throws Exception {
Assert.assertThat(6, CoreMatchers.is(6));
}

@Test
public void testAssertThat2() throws Exception {
Assert.assertThat(null, IsNull.nullValue());
}

@Test
public void testAssertThat3() throws Exception {
Assert.assertThat("Hello python world",CoreMatchers.both(CoreMatchers.startsWith("Hello")).and(CoreMatchers.endsWith("World")));
}

public class MyRule implements TestRule {

@Override
public Statement apply(final Statement base, final Description description) {

return new Statement() {
@Override
public void evaluate() throws Throwable {
// evaluate前执行方法相当于@Before
String methodName = description.getMethodName(); // 获取测试方法的名字
System.out.println(methodName + "测试开始!");

base.evaluate(); // 运行的测试方法

// evaluate后执行方法相当于@After
System.out.println(methodName + "测试结束!");
}
};
}
}
}


public class RepeatRule implements TestRule { //这里定义一个注解,用于动态在测试方法里指定重复次数
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Repeat {
int count();
}

@Override
public Statement apply(final Statement base, final Description description) {
Statement repeatStatement = new Statement() {
@Override
public void evaluate() throws Throwable {
Repeat repeat = description.getAnnotation(Repeat.class); //如果有@Repeat注解,则会重复执行指定次数
if (repeat != null) {
for (int i = 0; i < repeat.count(); i++) {
base.evaluate();
}
} else { //如果没有注解,则不会重复执行
base.evaluate();
}
}
};
return repeatStatement;
}
}


public class RepeatTest {
@Rule
public final RepeatRule repeatRule = new RepeatRule(); //该方法重复执行5次

@RepeatRule.Repeat(count = 5)
@Test
public void testMethod() throws IOException {
System.out.println("---test method---");
}

@Test
public void testMethod2() throws IOException {
System.out.println("---test method2---");
}
}

测试方法的执行顺序

当我们运行一个测试类里的所有测试方法时,测试方法的执行顺序并不是固定的,JUnit4提供@ FixMethodOrder注解来配置执行顺序,其可选值有:MethodSorters.NAME_ASCENDING、MethodSorters.DEFAULT、MethodSorters.JVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestExecOrder {
@Test
public void testD() {
System.out.println("DDDDD");
}

@Test
public void testA() {
System.out.println("AAAAA");
}

@Test
public void testB() {
System.out.println("BBBBB");
}

@Test
public void testC() {
System.out.println("CCCCC");
}
}

Test runners

所有的单元测试方法都是通过Runner来执行的。Runner只是一个抽象类,它是用来跑测试用例并通知结果的,JUnit提供了很多Runner的实现类,可以根据不同的情况选择不同的test runner。

通过@RunWith注解,可以为我们的测试用例选定一个特定的Runner来执行。
默认的test runner是 BlockJUnit4ClassRunner。
@RunWith(JUnit4.class),使用的依然是默认的test runner,实质上JUnit4继承自BlockJUnit4ClassRunner。

Suite

Suite 翻译过来是测试套件,意思是让我们将一批其他的测试类聚集在一起,然后一起执行,这样就达到了同时运行多个测试类的目的。

1
2
3
4
5
6
7
8
9
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestLogin.class,
TestLogout.class,
TestUpdate.class
})
public class TestSuite {
//不需要有任何实现方法
}

执行运行TestSuite,相当于同时执行了这3个测试类。
Suite还可以进行嵌套,即一个测试Suite里包含另外一个测试Suite。

1
2
3
4
@RunWith(Suite.class)
@Suite.SuiteClasses(TestSuite.class)
public class TestSuite2 {
}

Parameterized 参数化

假如我们有一个待测试类

1
2
3
4
5
6
7
8
9
10
11
public class Fibonacci {
public static int compute(int n) {
int result = 0;
if (n <= 1) {
result = n;
} else {
result = compute(n - 1) + compute(n - 2);
}
return result;
}
}

针对这个函数,我们需要多个输入参数来验证是否正确

  1. 使用构造函数来注入参数值
    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
     //指定Parameterized作为test runner 
    @RunWith(Parameterized.class)
    public class TestParams {
    //这里是配置参数的数据源,该方法必须是public static修饰的,且必须返回一个可迭代的数组或者集合
    // JUnit会自动迭代该数据源,自动为参数赋值,所需参数以及参数赋值顺序由构造器决定。
    @Parameterized.Parameters
    public static List getParams() {
    return Arrays.asList(new Integer[][]{{0, 0}, {1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5}, {6, 8}});
    }

    private int input;
    private int expected; //在构造函数里,指定了2个输入参数,JUnit会在迭代数据源的时候,自动传入这2个参数。

    // 例如:当获取到数据源的第3条数据{2,1}时,input=2,expected=1
    public TestParams(int input, int expected) {
    this.input = input;
    this.expected = expected;
    }

    @Test
    public void testFibonacci() {
    System.out.println(input + "," + expected);
    Assert.assertEquals(expected, Fibonacci.compute(input));
    }
    }

  2. 使用注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @RunWith(Parameterized.class)
    public class TestParams2 {
    @Parameterized.Parameters
    public static List getParams() {
    return Arrays.asList(new Integer[][]{{0, 0}, {1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5}, {6, 8}});
    }

    //这里必须是public,不能是private
    @Parameterized.Parameter
    public int input;

    //注解括号里的参数,用来指定参数的顺序,默认为0
    @Parameterized.Parameter(1)
    public int expected;

    @Test
    public void testFibonacci() {
    System.out.println(input + "," + expected);
    Assert.assertEquals(expected, Fibonacci.compute(input));
    }
    }

    Categories

    Categories继承自Suite,但是比Suite功能更加强大,它能对测试类中的测试方法进行分类执行。当你想把不同测试类中的测试方法分在一组,Categories就很管用。

代码 github地址

引用:
Android单元测试(一):JUnit框架的使用
Android单元测试