Android 单元测试实践

3,418 阅读7分钟

什么是单元测试

定义:单元测试就是针对最小的功能单元编写测试代码

Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。

什么是JUnit

JUnit是一个开源的Java语言的单元测试框架,专门针对Java设计,使用最广泛。JUnit是事实上的单元测试的标准框架,任何Java开发者都应当学习并使用JUnit编写单元测试。

使用JUnit编写单元测试的好处在于:

  • 非常简单地组织测试代码,并随时运行它们
  • JUnit会给出成功的测试和失败的测试,还可以生成测试报告
  • 不仅包含测试的成功率,还可以统计测试的代码覆盖率,即被测试的代码本身有多少经过了测试。
  • 几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写并运行JUnit测试

对于高质量的代码来说,测试覆盖率应该在80%以上。

单元测试的好处

  • 单元测试可以确保单个方法按照正确预期运行。如果修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动正确。
  • 测试代码本身就可以作为示例代码,用来演示如何调用该方法

在编写单元测试的时候,我们要遵循一定的规范:

  • 单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试;
  • 每个单元测试应当互相独立,不依赖运行的顺序;
  • 测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为0,null,空字符串""等情况。

线上更多暴露的都是异常场景,所以在单元测试中有必要重点验证相关异常逻辑。

如何编写单元测试

添加依赖

新建Android项目中app模块的build.gradle中会自动添加如下依赖:

testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
  • testImplementation :表示Junit单元测试依赖,对应的是test目录
  • androidTestImplementation :表示Android集成测试,对应的是androidTest目录

在写单元测试的时候,有些对象在运行时是没有真实构造的,这个时候我们可以使用mock框架来模拟出一个可用的对象,需要添加如下依赖:

testImplementation 'org.mockito:mockito-core:2.19.0'

添加用例

首先添加一个测试类,这里我添加一个简单的计算类:

public class Calculate {

    private int mValue;

    //+1
    public int addOne() {
        return ++mValue;
    }

    //-1
    public int reduceOne() {
        return --mValue;
    }
}

然后在方法名上右键鼠标,如下图所示,点击"Test":

goto test

如果之前该类没有创建过Test类,则会提示你没有找到对应的测试类,点击“create Test”即会出现如下弹框:

create test

  • Testing Library:测试用例库,因为我们build.gradle中依赖的是Junit4,所以这里选择Junit4即可
  • Class name:表示生成的测试文件类型。一般用默认的即可(业务类后面加上Test作为测试类名)
  • Superclass:基类名称。一般正常填业务类的基类即可
  • Destination package:test目录下生成的Test类的目标包名
  • setUp/@Before : 是否生成setUp方法,并且加上@Before注解
  • tearDown/@After :是否生成tearDwon方法,并且加上@After注解
  • Member:这里会列出该类提供的所有public方法,这里你可以选择对哪些方法添加测试用例

点击ok按钮,会让你选择创建单元测试用例 还是 集成测试用例,如下图所示:

file

这里我们选择单元测试用例。 然后我们就会在test目录下找到对应的包名和测试文件了,如下图所示:

file

注解

单元测试的时候用的最多的是上面3个注解:

@Before : 表示该方法在其他所有的Test方法执行之前都会执行一遍。一般用于初始化。

@After :表示每个Test方法执行结束后,都会执行一遍After方法。一般用于回收相关资源

@Test:标识该方法是一个测试方法

添加用例

我们在刚才生成的CalculateTest类中增加如下代码:

public class CalculateTest {

    private Calculate mCalculate;
    @Before
    public void setUp() throws Exception {
        mCalculate = new Calculate();
    }

    @After
    public void tearDown() throws Exception {
        mCalculate = null;
    }

    @Test
    public void addOne() {
        Assert.assertTrue(mCalculate.addOne() == 1);

        Assert.assertEquals(mCalculate.addOne(), 2);
        
    }

    @Test
    public void reduceOne() {
        Assert.assertTrue(mCalculate.reduceOne() == -1);
    }
}
  1. 我们首先声明一个Calculate类型的变量mCalculate
  2. 我们在setUp中构造一个Calculate对象实例,赋值给mCalculate
  3. 在addOne和reduceOne方法中引用mCalculate,做对应方法的验证

这里我们用到了Junit支持的断言来判断用例是否通过:

  • Assert.assertTrue:支持条件验证,条件满足则该用例能通过,否则用例执行会失败
  • Assert.assertEquals:这里assertEquals重载了多个类型的实现,只是这里是比较int值而已。

异步测试

public class CalculateTest {

    private Calculate mCalculate;

    ExecutorService sSingleExecutorService = Executors.newSingleThreadExecutor();

   	......

    @Test
    public void addOneAsync() {
        final CountDownLatch signal =  new  CountDownLatch(1) ;

        sSingleExecutorService.execute(new Runnable() {
            @Override
            public void run() {
                Assert.assertTrue(mCalculate.addOne() == 1);

                Assert.assertEquals(mCalculate.addOne(), 2);

                signal.countDown();
            }
        });

        try  {
            signal.await();
        }  catch  (InterruptedException e) {
            e.printStackTrace() ;
        }
    }
}

如上代码所示,针对异步场景,我们可以使用到 CountDownLatch 类来针对性的暂停执行线程,直到任务执行完成后再唤醒用例线程。

注意,上面的try 才是暂停执行线程的核心。

Mock测试

有些时候我们不免会引用Android框架的对象,但是我们单元测试又不是运行在真实设备上的,在运行时是没有构建出真实的Android对象的,不过我们可以通过mock程序来模拟一个假的对象,并且强制让该对象的接口返回我们预期的结果。

1.添加mock依赖引用,前面添加依赖项的时候有提到:

testImplementation 'org.mockito:mockito-core:2.19.0'

2.导入静态会让代码简洁很多,这步不是必要的:

import static org.mockito.Mockito.*;

3.创建mock对象

TextView mockView = mock(TextView.class);

4.进行测试插桩

when(mockView.getText()).thenReturn("Junit Test");

下面我们看一个简单的例子。 首先我们在Calculate 类中新增一个简单的方法,获取TextView的文本信息:

public String getViewString(TextView view) {
        return view.getText().toString();
    }

然后我们在CalculateTest类中新增测试方法:

	@Test
    public void mockTest() {
        TextView mockView = mock(TextView.class);

        when(mockView.getText()).thenReturn("Junit Test");

        assertEquals(mCalculate.getViewString(mockView), "Junit Test");
    }

最后运行这个用例,正常通过。

参数化测试

当一个方法有参数时,我们可以批量验证不同参数值,对应的用例是否通过,而不用写多遍类似的代码

1.首先参数化测试,要求我们对测试类添加如下注解

@RunWith(Parameterized.class)

2.定义参数集合 - 方法必须定义为 public static 的 - 必须添加@Parameterized.Parameters 3.定义接收参数和期望参数对象 4.增加对应的用例

我们看下面的例子: 首先我们在Calculate 中添加一个有参数的add方法:

public class Calculate {

    private int mValue;
	......
    public int add(int other) {
        mValue += other;
        return mValue;
    }
}

接着修改测试类

@RunWith(Parameterized.class) //---------@1
public class CalculateTest {

    private Calculate mCalculate;

    private Integer mInputNumber; //---------@3
    private Integer mExpectedNumber;

	//---------@4
    public CalculateTest(Integer input , Integer output) {
        mInputNumber = input;
        mExpectedNumber = output;
    }

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

    @Before
    public void setUp() throws Exception {
        mCalculate = new Calculate();
    }

    @After
    public void tearDown() throws Exception {
        mCalculate = null;
    }

	//---------@5
    @Test
    public void paramsTest() {
        assertEquals(mExpectedNumber, Integer.valueOf(mCalculate.add(mInputNumber)));
    }
}

@1 : 给类添加注解RunWith(Parameterized.class)

@2 : 添加数据集合方法,用@Parameterized.Parameters 注解修饰

@3 : 添加输入参数和期望参数

@4 : 添加构造方法,供给输入参数和期望参数赋值

@5 : 添加测试方法,直接使用输入参数和期望参数进行验证

异常测试

异常验证通过@Test注解参数来指定:

@Test(expected = InvalidParameterException.class)

看下面具体的例子:

public class Calculate {

    private int mValue;

    public int addException(int other) {
        if (other < 0) {
            throw  new InvalidParameterException();
        }

        return add(other);
    }
}

测试类如下:

@RunWith(Parameterized.class)
public class CalculateTest {

    private Calculate mCalculate;

    @Test(expected = InvalidParameterException.class)
    public void exceptionTest() {
        mCalculate.addException(-1);
    }
}

这里可以注意以下几点:

  • expected的异常如果是抛出异常的基类,用例测试也是可以通过的
  • 若没有添加expected参数,则用例会失败

运行用例

  • 运行单个用例方法

运行单个用例方法

点击左侧绿色箭头,会弹出如上图菜单,单机Run 即可执行该用例。

  • 批量执行某个类的所有用例

file

如上图所示,选中测试类文件,右键执行 "Run 类名",就会批量执行该类所有的用例了

  • 批量执行项目所有用例

file

如上图所示,右键包名,执行"Run Test in 包名" 即可执行该包下所有类对应的用例

导出测试报告

在执行完测试用例之后,我们可以导出测试报告,如下图所示:

导出测试报告

查看测试覆盖度

查看测试覆盖度

如上图所示:点击converage按钮,在右边窗口会弹出如下覆盖情况,这里从3个方面统计测试覆盖度:

  • class
  • method
  • Line

最后,我们可以导出覆盖报告.

本文由博客一文多发平台 OpenWrite 发布!