参数化测试及 Mockito 常用方法总结

记录一下,工作中编写单元测试的常用方法。

参数化测试

参数化是自动化测试的一种常用技巧,可以将测试代码中的某些输入使用参数来代替。

一、准备一个被测试类
public class Sample {

    public int add(int numA, int numB) {
        return numA + numB;
    }
}
二、生成对应的测试类
public class SampleTest {

    private Sample sample;
    
    @Before
    public void setUp() throws Exception {
        sample = new Sample();
    }

    @Test
    public void add()  {
        assertEquals(3, sample.add(1, 2));
    }

}
三、测试用例编写

我们可能会这样编写:

@Test
 public void add()  {
     assertEquals(3, sample.add(1, 2));
     assertEquals(4, sample.add(2, 2));
     assertEquals(5, sample.add(3, 2));
     assertNotEquals(6, sample.add(1, 2));
 }

这时候问题就出现了,以上写法的问题是这些测试用例不是纯粹的“单元",而是变成系列了,有先后关系了。整个测试过程只执行了一次 setUp() 操作,所以说每个测试用例的环境是不一样的。
那么如何做到纯粹的单元测试呢?那就是每个方法每次执行只测一个用例,然后就执行 tearDown(),当然我们不会每次执行完之后再去修改assertEquals(3, sample.add(1, 2)) 中的参数,这样效率太低。

这个时候我们就可以通过参数化标注的手段来实现多组纯粹的单元测试。。

四、参数化测试

首先在测试类上添加注解 @RunWith(Parameterized.class),在创建一个由 @Parameters 注解的 public static 方法,让返回一个对应的测试数据集合。最后创建构造方法,方法的参数顺序和类型与测试数据集合一一对应。

@RunWith(Parameterized.class)
public class SampleTest {

    private Sample sample;
    int expected = 0;
    int input1 = 0;
    int input2 = 0;

    @Parameterized.Parameters
    public static java.util.Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
                {3, 1, 2},
                {4, 2, 2},
                {0, -2, 2}
        });
    }

    public SampleTest(int expected, int input1, int input2) {
        this.expected = expected;
        this.input1 = input1;
        this.input2 = input2;
    }

    @Before
    public void setUp() throws Exception {
        sample = new Sample();
    }

    @Test
    public void add() throws Exception {
        assertEquals(expected, sample.add(input1, input2));
    }

}

通过参数化测试,可以保证测试方法每次执行的之后只会测试一个测试用例,测试完成之后执行 tearDown() 做清理工作,保证每个测试用例不会相互影响。
实现的逻辑是参数化运行器每次读取一组数据的时候都会重新通过传入数据组给测试类构造器,从而创建一个新对象,然后执行 setUp()、“testYourMethod()”、tearDown()操作。

Mockito

Mockito 是一个流行的开源框架,用于在软件测试中模拟对象。使用 Mockito 大大简化了具有外部依赖关系的类的测试开发。
模拟对象是接口或类的虚拟实现。它允许定义某些方法调用的输出。在测试环境中,用来替换掉真实的对象,以达到两大目的:

  • 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等。
  • 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作。

引入依赖及版本:

mockitoVersion=3.12.4
testImplementation ("org.mockito:mockito-core:$rootProject.ext.mockitoVersion")
testImplementation ("org.mockito:mockito-inline:$rootProject.ext.mockitoVersion")

Mockito 提供了几种创建模拟对象的方法:

1. 将 JUnit 5 的 @ExtendWith(MockitoExtension.class)扩展与字段上的 @Mock 注释结合使用(目前使用的是 JUnit4,暂忽略)
2. 使用静态mock()方法。
3. 使用@Mock注释。

好了,下面来看常用方法:

一、Mock 静态方法
    @Test
    fun getBtnColor() {
        Mockito.mockStatic(StatuesUtil::class.java).use {
            `when`(StatuesUtil.getSystemSwitchStatues("type1")).thenReturn(true)
            Assert.assertEquals(
                presenter.getBtnColor("type1"),
                R.color.display_red1
            )
        }
    }
注:这里 getBtnColor() 中有用到 StatuesUtil 里面的静态方法,所以这里通过 Mock 做了一个挡板,
当运行到 `StatuesUtil.getSystemSwitchStatues("type1")`,就会直接返回 true。

如果参数化测试的时候,需要 Mock 静态方法这时就需要在用完去关闭,代码如下:

...
    val mockitoStatuesUtil = Mockito.mockStatic(StatuesUtil::class.java)
    `when`(StatuesUtil.getScreenData()).thenReturn(1)
...
     mockitoStatuesUtil.close()
...

普通类的 final 方法,方式同静态方法。

二、Mock 单例类

有两种形式:
1. 单例类里面

  val mocked:DisplayCache = mock(DisplayCache::class.java)
  `when`(mocked.source).thenReturn(create().dataIntegrationList)

注:如果在单例类 DisplayCache 里面,可直接通过 mock 这种形式,直接挡板,返回模拟数据。

2. 其他类中(非 DisplayCache 类中)

...
    val displayCache: DisplayCache = DisplayCache.getInstance()
    val spy: DisplayCache = Mockito.spy(displayCache)
    spy.addAll(params3, "系统名")
...
注:  addAll() 为单例类里面的方法。
区别:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。
三、Mock 私有方法

通过反射验证 DisplayTemporaryData 类中的私有方法 screenData(),代码如下:

...
   val displayTemporaryData: DisplayTemporaryData = DisplayTemporaryData.getInstance()
   val spy: DisplayTemporaryData = Mockito.spy(displayTemporaryData)

   val method: Method = DisplayTemporaryData::class.java.getDeclaredMethod(
         "screenData",
         MutableList::class.java, String::class.java, Boolean::class.java
   )
   method.isAccessible = true
   method.invoke(spy, list, params2, params3)
...
注:私有方法对于测试是不可见的,它们一般也不能被 mock(如果不是通过反射)。

当然这里也可通过 PowerMockito 方式,代码如下:

public class ClassUnderTest {
    public boolean callPrivateMethod() {
        return isExist();
    }       
    private boolean isExist() {
        return false;
    }
}

@RunWith(PowerMockRunner.class)
public class TestClassUnderTest {
    @Test
    @PrepareForTest(ClassUnderTest.class)
    public void testCallPrivateMethod() throws Exception {
       ClassUnderTest underTest = PowerMockito.mock(ClassUnderTest.class);
       PowerMockito.when(underTest.callPrivateMethod()).thenCallRealMethod();
       PowerMockito.when(underTest, "isExist").thenReturn(true);
       Assert.assertTrue(underTest.callPrivateMethod());
    }
}
注:用 `PowerMockito` 还需要添加依赖,而且
四、其他常用方法

单元测试的目的是为了测试我们自己写的代码的正确性,它不需要测试外部的各种依赖,所以当我们遇到一个方法中有很多别的对象的依赖的时候,比如操作数据库,连接网络,读写文件等等,需要给它解依赖。
怎么解依赖呢?其实就是弄一些假对象,比如代码中是我们从网络获取一段json数据,转化成一个对象传入到我们的测试方法中。那么就可以直接 new 一个假的对象,并给它设置我们期望的返回值传给要测试的方法就好了,不需要再去请求网络获取数据。这个过程称之为 mock
Mockito 中存在着两种类型:mockspy
1. 结果验证
测试某些结果是否正确,使用 whenthenReturn 可根据传递给方法的参数,mock 可以返回不同的值。最后通过 assertEquals 判断返回值是否正确,代码如下:

@Test
    public void testMockitoResult() {
        Person person = mock(Person.class);
        //当调用person.getAge()方法的时候,给它返回一个18
        when(person.getAge()).thenReturn(18);
        //当调用person.getName()方法的时候,给它返回一个Lily
        when(person.getName()).thenReturn("Lily");
        //判断返回跟预期是否一样
        assertEquals(18, person.getAge());
        assertEquals("Lily", person.getName());
    }

2. 验证行为
有时候会测试某些行为是否被执行过,通过 verify 方法可以验证某个方法是否执行过,执行的次数:

@Test
    public void testMockitoBehavior() {
        Person person = mock(Person.class);
        int age = person.getAge();
        //验证getAge动作有没有发生
        verify(person).getAge();
        //验证person.getName()是不是没有调用
        verify(person, never()).getName();
        //验证是否最少调用过一次person.getAge
        verify(person, atLeast(1)).getAge();
        //验证getAge动作是否被调用了2次,前面只用了一次所以这里会报错
        verify(person, times(2)).getAge();
    }

3. mock 默认值
通过 Mockito mock一个 Person 对象,那么这个对象的 name 属性是默认为 null 的,如果我们不想让它为 null ,默认为空字符串可以使用 RETURNS_SMART_NULLS

@Test
    public void testNotNull(){
        Person person = mock(Person.class);
        System.out.println(person.getName());
        Person person1 = mock(Person.class,RETURNS_SMART_NULLS);
        System.out.println(person1.getName());
    }

4. @Mock注解
使用 @Mock 注解来 mock 一个对象:

@Mock
    List<Integer> mList;
    @Test
    public void testAnnotationMock(){
        mList.add(0);
        verify(mList).add(0);
    }

5. 验证是否执行了某个参数的方法

@Test
    public void testParameter(){
        Person person = mock(Person.class);
        when(person.getDuty(1)).thenReturn("医生");
        System.out.println(person.getDuty(1));
        //anyInt任何Int值,此外还有anyString,anyFloat等
        when(person.getDuty(anyInt())).thenReturn("护士");
        System.out.println(person.getDuty(anyInt()));
        //验证person.getDuty(1)方法有没有调用
        verify(person).getDuty(ArgumentMatchers.eq(1));
    }

6. mock 真实对象
mock 出来的对象都是虚拟的对象,我们可以验证其执行次数,状态等,如果一个对象是真实的,那怎么验证呢 可以使用 spy 包装一下。
spy 对象的方法默认调用真实的逻辑,mock 对象的方法默认什么都不做,或直接返回默认值,代码如下:

@Test
    public void testSpy(){
        Person person = getPerson();
        Person spy = spy(person);
        when(spy.getName()).thenReturn("Lily");
        System.out.println(spy.getName());
        verify(spy).getName();
    }
    private Person getPerson(){
        return new Person();
    }

最后,参考链接:

Android 单元测试第二篇(参数化测试)
Android自动化测试入门(四)单元测试

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容