美文网首页
单元测试介绍

单元测试介绍

作者: SunnyDay_ab5f | 来源:发表于2022-10-18 19:03 被阅读0次

1. 单元测试介绍

单元测试是应用程序测试策略中的基本测试,通过对代码进行单元测试,可以轻松地验证单个单元的逻辑是否正确,在每次构建之后运行单元测试,可以帮助您快速捕获和修复因代码更改(重构、优化等)带来的回归问题。本文主要聊聊Android中的单元测试

1.1为什么要进行单元测试?

提高稳定性,能够明确地了解是否正确的完成开发;
快速反馈bug,跑一遍单元测试用例,定位bug;
在开发周期中尽早通过单元测试检查bug,最小化技术债,越往后可能修复bug的代价会越大,严重的情况下会影响项目进度;
为代码重构提供安全保障,在优化代码时不用担心回归问题,在重构后跑一遍测试用例,没通过说明重构可能是有问题的,更加易于维护。

1.2单元测试要测什么?

  • 列出想要测试覆盖的正常、异常情况,进行测试验证;
  • 性能测试,例如某个算法的耗时等等。

1.3单元测试的分类

  • 本地测试(Local tests): 只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的[Mockito][1];

  • 仪器化测试(Instrumented tests): 在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式。

2.Junit注解介绍

JUnit是Java最基础的测试框架,主要的作用就是断言。使用时在app的build文件中添加依赖。

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

Assert类中主要方法如下:

方法名 方法描述
assertEquals 断言传入的预期值与实际值是相等的
assertNotEquals 断言传入的预期值与实际值是不相等的
assertArrayEquals 断言传入的预期数组与实际数组是相等的
assertNull 断言传入的对象是为空
assertNotNull 断言传入的对象是不为空
assertTrue 断言条件为真
assertFalse 断言条件为假
assertSame 断言两个对象引用同一个对象,相当于“==”
assertNotSame 断言两个对象引用不同的对象,相当于“!=”
assertThat 断言实际值是否满足指定的条件

注意:上面的每一个方法,都有对应的重载方法,可以在前面加一个String类型的参数,表示如果断言失败时的提示。

JUnit 中的常用注解:

注解名 含义
@Test 表示此方法为测试方法
@Before 在每个测试方法前执行,可做初始化操作
@After 在每个测试方法后执行,可做释放资源操作
@Ignore 忽略的测试方法
@BeforeClass 在类中所有方法前运行。此注解修饰的方法必须是static void
@AfterClass 在类中最后运行。此注解修饰的方法必须是static void
@RunWith 指定该测试类使用某个运行器
@Parameters 指定测试类的测试数据集合
@Rule 重新制定测试类中方法的行为
@FixMethodOrder 指定测试类中方法的执行顺序

执行顺序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass

assertThat用法

上面我们所用到的一些基本的断言,如果我们没有设置失败时的输出信息,那么在断言失败时只会抛出AssertionError,无法知道到底是哪一部分出错。而assertThat就帮我们解决了这一点。它的可读性更好。

assertThat(T actual, Matcher<? super T> matcher);

assertThat(String reason, T actual, Matcher<? super T> matcher);

其中reason为断言失败时的输出信息,actual为断言的值,matcher为断言的匹配器。

常用的匹配器整理:

匹配器 说明例子
is 断言参数等于后面给出的匹配表达式 assertThat(5, is (5));
not 断言参数不等于后面给出的匹配表达式 assertThat(5, not(6));
equalTo 断言参数相等 assertThat(30, equalTo(30));
equalToIgnoringCase 断言字符串相等忽略大小写 assertThat(“Ab”, equalToIgnoringCase(“ab”));
containsString 断言字符串包含某字符串 assertThat(“abc”, containsString(“bc”));
startsWith 断言字符串以某字符串开始 assertThat(“abc”, startsWith(“a”));
endsWith 断言字符串以某字符串结束 assertThat(“abc”, endsWith(“c”));
nullValue 断言参数的值为null assertThat(null, nullValue());
notNullValue 断言参数的值不为null assertThat(“abc”, notNullValue());
greaterThan 断言参数大于 assertThat(4, greaterThan(3));
lessThan 断言参数小于 assertThat(4, lessThan(6));
greaterThanOrEqualTo 断言参数大于等于 assertThat(4, greaterThanOrEqualTo(3));
lessThanOrEqualTo 断言参数小于等于 assertThat(4, lessThanOrEqualTo(6));
closeTo 断言浮点型数在某一范围内 assertThat(4.0, closeTo(2.6, 4.3));
allOf 断言符合所有条件,相当于&& assertThat(4,allOf(greaterThan(3), lessThan(6)));
anyOf 断言符合某一条件,相当于或 assertThat(4,anyOf(greaterThan(9), lessThan(6)));
hasKey 断言Map集合含有此键 assertThat(map, hasKey(“key”));
hasValue 断言Map集合含有此值 assertThat(map, hasValue(value));
hasItem 断言迭代对象含有此元素 assertThat(list, hasItem(element));

注意:assertThat()是Assert类中的静态方法现在已经被废弃,推荐使用 MatcherAssert.assertThat()

JUnit 中@Rule注解介绍
利用 @Rule 我们可以扩展 Junit 的功能,在执行case的时候加入测试者特有的操作,而不影响原有case的代码,减少了特有操作和test case原逻辑的耦合。

@Rule 只能注解在字段中,该字段必须是 public 的并且类型必须实现了 TestRule 接口或者 MethodRule 接口。

Junit 4.9之后还加入了一个 @ClassRule 注解。相对 @Rule 来说, @ClassRule 是一个类级别的注解。就像 @Before 与 @BeforeClass 的区别。

下面是Junit自带的实现TestRule接口的类

   //生成临时文件或临时文件夹 必须是public修饰
    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    //在一个运行测试方法过程中收集多个错误信息
    @Rule
    public ErrorCollector errorCollector = new ErrorCollector();

    //设置执行最长时间
    @Rule
    public Timeout timeout = new Timeout(1000, TimeUnit.MILLISECONDS);

    //取得当前的测试方法名称
    @Rule
    public TestName testName = new TestName();

以上类的用法示例,比如打印当前方法名:

    @Rule
    public TestName testName = new TestName();

    @Test
    public void isEmail_1() {
        System.out.println("方法名称 = "+testName.getMethodName());
        TestUtil testUtil = new TestUtil();
        boolean result = testUtil.isEmail("123");
        Assert.assertEquals(false,result);
    }

当然我们也可以实现自定义的Rule

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 + "测试结束!");
            }
        };
    }
}
    //自定义Rule打印方法开始执行和方法执行结束
    @Rule
    public MyRule myRule = new MyRule();


    @Test
    public void isEmail_1() {
        System.out.println("方法名称 = "+testName.getMethodName());
        TestUtil testUtil = new TestUtil();
        boolean result = testUtil.isEmail("123");
        Assert.assertEquals(false,result);
    }

执行测试方法结果如下图:

image.png

使用RuleChain
RuleChain提供一种将多个TestRule串在一起执行的机制。这在JUnit 4.10以后的版本中可以使用。需要根据特定顺序执行多个处理的时候,用RuleChain可以提高效率

public class MyRule2 implements TestRule {

    public String str = "";

    public MyRule2(String str) {
        this.str = str;
    }

    @NonNull
    @Override
    public Statement apply(@NonNull Statement base, @NonNull Description description) {
        System.out.println(str);
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                base.evaluate();
            }
        };
    }
}
    @Rule
    public TestRule testRule = RuleChain.outerRule(new MyRule2("----------111"))

            .around(new MyRule2("----------222"))

            .around(new MyRule2("----------333"));

    @Test
    public void isEmail_2() {
        TestUtil testUtil = new TestUtil();
        boolean result = testUtil.isEmail("abc@163.com");
        Assert.assertEquals(true,result);
    }
image.png

3.Mockito介绍

使用时在build文件中添加依赖。

dependencies {
    testImplementation "org.mockito:mockito-core:3.3.3"
    androidTestImplementation 'org.mockito:mockito-android:3.3.3'
}

Mockito创建对象的方式

  • 普通方法
public class MockitoTest {
    @Test
    public void testIsNotNull(){
        Person mPerson = mock(Person.class); //<--使用mock方法
        assertNotNull(mPerson);
    }
}

  • 注解方法
public class MockitoAnnotationsTest {
    @Mock //<--使用@Mock注解
    Person mPerson;
    @Before
    public void setup(){
        MockitoAnnotations.initMocks(this); //<--初始化
    }
    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }
}

  • 运行器方法:
@RunWith(MockitoJUnitRunner.class) //<--使用MockitoJUnitRunner
public class MockitoJUnitRunnerTest {
    @Mock //<--使用@Mock注解
    Person mPerson;

    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }

  • MockitoRule方法
public class MockitoRuleTest {
    @Mock //<--使用@Mock注解
    Person mPerson;

    @Rule //<--使用@Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }
}

常用打桩方法
因为Mock出的对象中非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。“打桩”顾名思义就是将我们Mock出的对象进行操作,比如提供模拟的返回值等,给Mock打基础。

方法名 方法描述
thenReturn(T value) 设置要返回的值
thenThrow(Throwable… throwables) 设置要抛出的异常
thenAnswer(Answer<?> answer) 对结果进行拦截
doReturn(Object toBeReturned) 提前设置要返回的值
doThrow(Throwable… toBeThrown) 提前设置要抛出的异常
doAnswer(Answer answer) 提前对结果进行拦截
doCallRealMethod() 调用某一个方法的真实实现
doNothing() 设置void方法什么也不做

关于Mockito.when(Method()).thenReturn(...) 与 Mockito.doReturn(...).when(...).Method()前者会执行到方法里,然后在将返回值Mock掉。 后者不会走到Methond()里,提前拦截直接返回,常用在void方法。

常用验证方法

前面所说的都是状态测试,但是如果不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。从概念上讲,就是和状态测试所不同的“行为测试”了。

verify(T mock)验证发生的某些行为 。

方法名 方法描述
after(long millis) 在给定的时间后进行验证
timeout(long millis) 验证方法执行是否超时
atLeast(int minNumberOfInvocations) 至少进行n次验证
atMost(int maxNumberOfInvocations) 至多进行n次验证
description(String description) 验证失败时输出的内容
times(int wantedNumberOfInvocations) 验证调用方法的次数
never() 验证交互没有发生,相当于times(0)
only() 验证方法只被调用一次,相当于times(1)

常用参数匹配器

方法名 方法描述
anyObject() 匹配任何对象
any(Class<T> type) 与anyObject()一样
any() 与anyObject()一样
anyBoolean() 匹配任何boolean和非空Boolean
anyByte() 匹配任何byte和非空Byte
anyCollection() 匹配任何非空Collection
anyDouble() 匹配任何double和非空Double
anyFloat() 匹配任何float和非空Float
anyInt() 匹配任何int和非空Integer
anyList() 匹配任何非空List
anyLong() 匹配任何long和非空Long
anyMap() 匹配任何非空Map
anyString() 匹配任何非空String
contains(String substring) 参数包含给定的substring字符串
argThat(ArgumentMatcher <T> matcher) 创建自定义的参数匹配模式

其他方法

方法名 方法描述
reset(T … mocks) 重置Mock
spy(Class<T> classToSpy) 实现调用真实对象的实现
inOrder(Object… mocks) 验证执行顺序
@InjectMocks注解 自动将模拟对象注入到被测试对象中

Mock 和 Spy 区别:
如果不指定的话,一个mock对象的所有非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等;而void方法将什么都不做。
总之,spy与mock的唯一区别就是默认行为不一样:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。

Demo:
//假设目标类的实现是这样的
  public class PasswordValidator {
      public boolean verifyPassword(String password) {
          return "xiaochuang_is_handsome".equals(password);
      }
  }

@Test
  public void testSpy() {
     //跟创建mock类似,只不过调用的是spy方法,而不是mock方法。spy的用法
     PasswordValidator spyValidator = Mockito.spy(PasswordValidator.class);
 
//在默认情况下,spy对象会调用这个类的真实逻辑,并返回相应的返回值,这可以对照上面的真实逻辑
     spyValidator.verifyPassword("xiaochuang_is_handsome"); //true
     spyValidator.verifyPassword("xiaochuang_is_not_handsome"); //false
     
     //spy对象的方法也可以指定特定的行为
     Mockito.when(spyValidator.verifyPassword(anyString())).thenReturn(true);
     
     //同样的,可以验证spy对象的方法调用情况
     spyValidator.verifyPassword("xiaochuang_is_handsome");
     Mockito.verify(spyValidator).verifyPassword("xiaochuang_is_handsome"); //pass
 }

4.PowerMock介绍

PowerMock是用来扩展Mockito功能,弥补其局限性(Mockito不能mock private、static方法和类,一些版本不能mock final方法和类),同时PowerMock还增加了很多反射方法来修改静态和非静态成员等。

PowerMock是依赖Mockito的,所以使用时要同时引入,且版本也必须一一对应。

PowerMock跟Mockito的版本对应关系如下:

Mockito PowerMock
2.8.9+ 2.x
2.8.0-2.8.9 1.7.x
2.7.5 1.7.0RC4
2.4.0 1.7.0RC2
2.0.0-beta - 2.0.42-beta 1.6.5-1.7.0RC
1.10.8 - 1.10.x 1.6.2 - 2.0
1.9.5-rc1 - 1.9.5 1.5.0 - 1.5.6
1.9.0-rc1 & 1.9.0 1.4.10 - 1.4.12
1.8.5 1.3.9 - 1.4.9
1.8.4 1.3.7 & 1.3.8
1.8.3 1.3.6
1.8.1 & 1.8.2 1.3.5
1.8 1.3
1.7 1.2.5

下面举个public方法中调用private方法的例子,验证private方法是否被调用

public class PowerMockTest {
   public int division(int x,int y){
      if (y == 0){
         return getResultByZeroDivisor();
      }else {
         return x/y;
      }
   }

   private int getResultByZeroDivisor(){
      return 0;
   }


}
@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockTest.class})
public class PowerMockTestTest {

    @Test
    public void division() {
        //mock
        PowerMockTest test = Mockito.spy(PowerMockTest.class);
        int division = test.division(1, 0);
        assertEquals(0,division);
    }

    @Test
    public void testPrivateMethod() throws Exception {
        //powermock
        PowerMockTest test = PowerMockito.spy(new PowerMockTest());
        test.division(1,0);
        PowerMockito.verifyPrivate(test,times(1)).invoke("getResultByZeroDivisor",new Object[]{});
    }
}

测试结果如下图:


image.png

这里改变下入参 y =1

    @Test
    public void testPrivateMethod() throws Exception {
        PowerMockTest test = PowerMockito.spy(new PowerMockTest());
        test.division(1,1);
        PowerMockito.verifyPrivate(test,times(1)).invoke("getResultByZeroDivisor",new Object[]{});
    }

结果报错了,因为private方法未被调用


image.png
  1. @RunWith(PowerMockRunner.class) :这里使用的Runner是PowerMockRunner,这样就可以与Mokito兼容了,即Mokito里的方法在这里也都是可以正常使用的;

  2. @PrepareForTest({Calculator.class}):务必记得加上这个PrepareForTest的注解,否则进行verify测试的时候怎么测试都是pass的,如下没有写这个注解,verifyPrivate()怎么测试都是pass。

常用测试方法

1.创建模拟对象的2种姿势

  • mock
activity = PowerMockito.mock(new MainActivity())
//使activity的isFinishing方法总是返回true
when(activity.isFinishing()).thenReturn(true);

通过mock创造出来的对象,调用该对象所有方法都不会执行真实逻辑。必须结合when(...).then(...)来使模拟对象按照我们预期返回。

  • spy
activity = PowerMockito.spy(new MainActivity())
//使activity的isFinishing方法总是返回false
PowerMockito.doReturn(false).when(activity).isFinishing();

通过spy创造模拟对象必须先手动new出来,调用该对象所有方法都会执行真实逻辑。
spy对象必须结合doReturn(...).when(...)才会忽略真实逻辑,并按照我们预期返回。如果函数返回值为void,可以用doNothing()代替doReturn()。

2.访问/调用private
有时候被测类绝大部分是private函数(比如Activity),传统的单元测试很难覆盖到这些private函数,当然我们可以通过重构/封装使我们的业务代码对测试更友好,但为了测试而对原本稳定的业务代码进行侵入式的修改,在短期内肯定会带来不稳定因素,这往往是团队/领导无法容忍的。

PowerMock的Whitebox类提供了一组api可以获取/修改private的变量和函数,可以帮助我们绕过重构去对业务代码进行测试。

//修改私有变量
Whitebox.setInternalState(..)
//访问私有变量
Whitebox.getInternalState(..)
//调用私有函数
Whitebox.invokeMethod(..)
//调用私有的构造函数
Whitebox.invokeConstructor(..) 

mock静态方法

   public static boolean isEmpty(String s){
      return s == null || s.length() == 0;
   }
    @Test
    public void isEmpty(){
        PowerMockito.mockStatic(PowerMockTest.class);
        PowerMockito.when(PowerMockTest.isEmpty("xxx")).thenReturn(false);
        assertFalse(PowerMockTest.isEmpty("xxx"));
    }

3.抑制不必要的代码逻辑执行
在实际项目中会有很多常用但不影响业务逻辑的代码(Log以及其他统计代码等等),有些静态代码块也直接调用Android SDK api。因为单元测试代码运行在JVM上,这些代码很容易会报错,如果为了测试去修改这些代码未免有点本末倒置,所以我们在单元测试的过程中需要抑制/隔离这些代码的执行

抑制静态变量/代码块的执行
PowerMockito提供了@SuppressStaticInitializationFor注解:

//在单元测试类之前声明以下注解,可以阻止FileUtil类的静态代码块运行
@SuppressStaticInitializationFor("com.colin.unittest.FileUtil")
public class PowerMockitoSampleIII {
    ...
}

抑制Log等静态函数的执行
借助mockStatic可以使指定类的静态方法不执行。

@PrepareForTest(Log.class)
public class PowerMockitoSampleIII {
    @Before
    public void setUp() throws Exception {
    //抑制Log相关代码的执行
    PowerMockito.mockStatic(Log.class);
    }
    ...
}

抑制super函数()的执行
实际业务开发中,我们经常需要继承Android SDK的类来进行扩展,对这些类覆写的函数进行单元测试时,往往需要抑制父类super()的逻辑,不然在JVM中执行单元测试代码时会报错。

//抑制MainActivity父类的onDestroy方法
Method method = PowerMockito.method(MainActivity.class.getSuperclass(),
    "onDestroy");
PowerMockito.suppress(method);

相关文章

  • Android单元测试

    本文主要内容 1、单元测试介绍 2、java单元测试 3、android单元测试 4、常用方法介绍 1、单元测试介...

  • 单元测试

    本文将介绍以下内容: iOS开发中添加单元测试的方法。 如何写单元测试用例及用例组。 介绍单元测试的一些基础概念。...

  • Android单元测试介绍

    本篇介绍Android相关的单元测试, 本篇为系列教程的开篇, 对Android单元测试做一个系统介绍. 本系列教...

  • Android单元测试详解

    前言:要做单元测试,首先要明白,什么是单元测试。关于单元测试的相关内容,这里就不多做介绍了,若要了解单元测试的相关...

  • Python 学习笔记 066

    单元测试 对函数进行单元测试 对类进行单元测试 文档测试 创建函数的解释和介绍 使用Telnet命令远程连接别人的...

  • 【python接口自动化】初识unittest框架

    本文将介绍单元测试的基础版及使用unittest框架的单元测试。 完成以下需求的代码编写,并实现单元测试 账号正确...

  • 第一本技术书籍《软件自动化测试开发》--知识点03单元测试Jun

    本篇将会主要对单元测试的主流框架 JUnit 和 TestNG做由浅入深的介绍单元测试概述单元测试是用来对一个模块...

  • 单元测试框架:Robolectric

    前言 前面我们介绍了单元测试框架 JUnit 和 Mockito 的使用(详情查看:单元测试框架:JUnit,单元...

  • Android单元测试(二)

    在上一篇文章中我们介绍了Android单元测试入门所需了解的内容,本文接上文继续学习单元测试相关框架。本文介绍了A...

  • 从零开始写博客系统——测试我们的代码(接口测试)

    背景 上文我们介绍了如何对我们的代码进行单元测试,本文我们来介绍接口测试。 相对于单元测试,接口测试属于一个更上层...

网友评论

      本文标题:单元测试介绍

      本文链接:https://www.haomeiwen.com/subject/bqbgartx.html