Android:如何快速引入单元测试

375 阅读11分钟

Android单元测试方案

Android单元测试方案

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

目的

提高稳定性,能够明确地了解是否正确的完成开发;

快速反馈bug,跑一遍单元测试用例,定位bug;

在开发周期中尽早通过单元测试检查bug,最小化技术债,越往后可能修复bug的代价会越大,严重的情况下会影响项目进度;

为代码重构提供安全保障,在优化代码时不用担心回归问题,在重构后跑一遍测试用例,没通过说明重构可能是有问题的,更加易于维护。

分类

本地测试(test):只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的.

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

本地测试

引包

testImplementation 'junit:junit:4.13.2'
Junit

JUnit提供了一些辅助函数,他们用来帮助我们确定被测试的方法是否按照预期正常执行,这些辅助函数我们称之为断言(Assertion)。JUnit4所有的断言都在org.junit.Assert类中,Assert类包含了一组静态的测试方法,用于验证期望值expected与实际值actual之间的逻辑关系是否正确,如果不符合我们的预期则表示测试未通过。

Junit5

项目 gradle配置

dependencies {

    classpath "de.mannodermaus.gradle.plugins:android-junit5:1.8.2.1"

}

app gradle配置

plugins {

id("de.mannodermaus.android-junit5")



}



// (Required) Writing and executing Unit Tests on the JUnit Platform

testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")

testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")



// (Optional) If you need "Parameterized Tests"

testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2")



// (Optional) If you also have JUnit 4-based tests

testImplementation("junit:junit:4.13.2")

testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")

参数化测试

​ 支持外部的各类入参

/**

 * csv文件内容:

 * shawn,24

 * uzi,50

 */

@ParameterizedTest

@CsvFileSource(resources = "/test.csv")  //指定csv文件位置

@DisplayName("参数化测试-csv文件")

public void parameterizedTest2(String name, Integer age) {

    System.out.println("name:" + name + ",age:" + age);

    Assertions.assertNotNull(name);

    Assertions.assertNotNull(age);

}



@ParameterizedTest

@MethodSource("method")    //指定方法名

@DisplayName("方法来源参数")

public void testWithExplicitLocalMethodSource(String name) {

    System.out.println(name);

    Assertions.assertNotNull(name);

}



static Stream<String> method() {

    return Stream.of("apple", "banana");

}

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@ParameterizedTest

@ValueSource(strings = {"one", "two", "three"})

@DisplayName("参数化测试1")

public void parameterizedTest1(String string) {

    System.out.println(string);

    Assertions.assertTrue(StringUtils.isNotBlank(string));

}

结构化测试

public class NestedTestDemo {



    @Test

    @DisplayName("Nested")

    void isInstantiatedWithNew() {

        System.out.println("最一层--内嵌单元测试");

    }



    @Nested

    @DisplayName("Nested2")

    class Nested2 {



        @BeforeEach

        void Nested2_init() {

            System.out.println("Nested2_init");

        }



        @Test

        void Nested2_test() {

            System.out.println("第二层-内嵌单元测试");

        }





        @Nested

        @DisplayName("Nested3")

        class Nested3 {



            @BeforeEach

            void Nested3_init() {

                System.out.println("Nested3_init");

            }



            @Test

            void Nested3_test() {

                System.out.println("第三层-内嵌单元测试");

            }

        }

    }



}
mockito

site.mockito.org

Mockito是一个用于java单元测试中的mocking框架,mock就是模拟的意思,就是能够模拟一些类和方法的实现

为什么需要mock?

在写单元测试的时候,我们会遇到某个测试类有很多依赖,这些依赖类或对象又有别的依赖,这样会形成一棵巨大的依赖树。ClassA并不关心ClassB的依赖对象ClassC和ClassE是怎么构造,它只关心ClassB和ClassD的构造

引包

//mockito

testImplementation 'org.mockito:mockito-core:4.9.0'
验证行为
@Test

    public void testMock() {

        //创建一个mock对象

        List list = mock(List.class);



        //使用mock对象

        list.add("one");

        list.clear();



        //验证mock对象的行为

        verify(list).add("one");  //验证有add("one")行为发生

        verify(list).clear();          //验证有clear()行为发生

    }
Stubbing
@Test

    public void testMock2() {

        //不仅可以针对接口mock, 还可以针对具体类

        LinkedList list = mock(LinkedList.class);



        //设置返回值,当调用list.get(0)时会返回"first"

        when(list.get(0)).thenReturn("first");

        //当调用list.get(1)时会抛出异常

        when(list.get(1)).thenThrow(new RuntimeException());



        //会打印"print"

        System.out.println(list.get(0));

        //会抛出RuntimeException

        System.out.println(list.get(1));

        //会打印 null

        System.out.println(list.get(99));



        verify(list).get(0);

    }

注意:

对于有返回值的方法,mock会默认返回null、空集合、默认值。比如为int/Integer返回0,为boolean/Boolean返回false、为Object返回null。

一旦stubbing,不管方法被调用多少次,都永远返回stubbing的值。

stubbing可以被覆盖, 如果对同一个方法进行多次stubbing,最后一次的stubbing会生效。

Argument matchers(参数匹配器)
@Test

    public void testMock3() {

        List list = mock(List.class);

        //使用anyInt(), anyString(), anyLong()等进行参数匹配

        when(list.get(anyInt())).thenReturn("item");



        //将会打印出"item"

        System.out.println(list.get(100));



        verify(list).get(anyInt());

    }
验证方法的调用次数
@Test

    public void testMock4() {

        List list = mock(List.class);

        list.add("once");

        list.add("twice");

        list.add("twice");

        list.add("triple");

        list.add("triple");

        list.add("triple");



        //执行1次

        verify(list, times(1)).add("once");

        //执行2次

        verify(list, times(2)).add("twice");

        verify(list, times(3)).add("triple");



        //从不执行, never()等同于times(0)

        verify(list, never()).add("never happened");



        //验证至少执行1次

        verify(list, atLeastOnce()).add("twice");

        //验证至少执行2次

        verify(list, atLeast(2)).add("twice");

        //验证最多执行4次

        verify(list, atMost(4)).add("triple");

    }
验证****回调

代码中调用的函数有可能是注册的回调,如registerCallback(callback),此时如果想对callback进行测试,可以使用thenAnswer

 @Mock

    private ExampleService exampleService;



    @Test

    public void test() {



        MockitoAnnotations.initMocks(this);



        when(exampleService.add(anyInt(),anyInt())).thenAnswer(new Answer<Integer>() {

            @Override

            public Integer answer(InvocationOnMock invocation) throws Throwable {

                Object[] args = invocation.getArguments();

                // 获取参数

                Integer a = (Integer) args[0];

                Integer b = (Integer) args[1];



                // 根据第1个参数,返回不同的值

                if (a == 1) {

                    return 9;

                }

                if (a == 2) {

                    return 99;

                }

                if (a == 3) {

                    throw new RuntimeException("异常");

                }

                return 999;

            }

        });



        Assert.assertEquals(9, exampleService.add(1, 100));

        Assert.assertEquals(99, exampleService.add(2, 100));



        try {

            exampleService.add(3, 100);

            Assert.fail();

        } catch (RuntimeException ex) {

            Assert.assertEquals("异常", ex.getMessage());

        }

    }
验证抛异常
  @Test

    public void test() {



        Random mockRandom = mock(Random.class);



        when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常"));



        try {

            mockRandom.nextInt();

            Assert.fail();  // 上面会抛出异常,所以不会走到这里

        } catch (Exception ex) {

            Assert.assertTrue(ex instanceof RuntimeException);

            Assert.assertEquals("异常", ex.getMessage());

        }



    }
使用spy()监视真正的对象

使用spy可以监视对象方法的真实调用。当我们mock某个类时,如果需要某些方法是真实调用,而某些方法是mock调用时,借助spy可以实现这些功能。

 @Test

    public void testMock10(){

        List list = new ArrayList();

        List spy = spy(list);



        //subbing方法,size()并不会真实调用,这里返回10

        when(spy.size()).thenReturn(10);



        //使用spy对象会调用真实的方法

        spy.add("one");

        spy.add("two");



        //会打印出"one"

        System.out.println(spy.get(0));

        //会打印出"10",与前面的stubbing方法对应

        System.out.println(spy.size());



        //对spy对象依旧可以来验证其行为

        verify(spy).add("one");

        verify(spy).add("two");

    }
doReturn/doThrow/doAnswer/doNothing

由于spy对象默认会走真实的函数,主要给spy对象使用,效果同前面的when().then()

对void方法不能使用when/thenReturn语法。

对spy对象要慎用when/thenReturn

 List spyList = spy(new ArrayList());



        //下面代码会抛出IndexOutOfBoundsException

        when(spyList.get(0)).thenReturn("foo");

       

        //这里不会抛出异常

        doReturn("foo").when(spyList).get(0);

        System.out.println(spyList.get(0));
缺点

原理:使用Cglib(内部使用ASM)框架来生成测试类的子类,用于屏蔽或执行测试类的方法。所以,对于final,private,static 类和方法,mockito是无能为力的,kotlin的类默认都是final,mockito不太适用。

Mockito简单使用及原理分析

blog.csdn.net/seasonLai/a…

MockK

MockK 是一个用 Kotlin 写的 Mocking 框架。

android studio配置

//mockk

testImplementation "io.mockk:mockk:1.10.5"
mock类
@Test

fun testDoSomething1() {

    val mockk = mockk<MockkClassSub>()

    //mock指定方法

    every { mockk.doSomething1(any()) } returns Unit

    val mockkClass = MockkClass(mockk)

    //调用被mock的方法

    mockk.doSomething1("")

    //该方法未通过every进行mock,会报错

    mockk.doSomething2("")

}

注意:和mockito不同,mockk的对象方法默认会抛异常

mock对象

mock静态类

@Test

fun testObject() {

    mockkStatic(Log::class)

    every { Log.d(any(), any()) } returns 0

    val classSub = MockkClassSub()

    mockkObject(classSub)



    //mock指定方法

    every { classSub.doSomething1(any()) } returns Unit

    val mockkClass = MockkClass(classSub)

    //调用被mock的方法

    mockkClass.testDoSomething1("")

    //调用真实的方法

    mockkClass.testDoSomething2("")

}
spyk() & spyk(T obj)
@Test

    fun testSpy() {

        mockkStatic(Log::class)

        every { Log.d(any(), any()) } returns 0

        val mockk = spyk<MockkClassSub>()

//        val mockkClassSub = MockkClassSub()

//        //返回mockkClassSub对象被spyk后的对象,原对象不会改变

//        val spyk1 = spyk(mockkClassSub)

        every { mockk.doSomething1(any()) } returns Unit



        val mockkClass = MockkClass(mockk)

        //调用被mock的方法

        mockkClass.testDoSomething1("")

        //调用真实的方法

        mockkClass.testDoSomething2("")

    }
mock私有方法

对于 Object 类,需要在mock的时候设置 recordPrivateCalls 为true

@Test

fun testPrivate() {

    mockkStatic(Log::class)

    every { Log.d(any(), any()) } returns 0

    val mockClass = MockkClass(MockkClassSub())

    val mockk = spyk(mockClass, recordPrivateCalls = true)

    every { mockk["testPrivate"]("") } returns Unit

    mockk.testPublic("")

    verify {

        println("verify testPrivate")

        mockk["testPrivate"]("")

    }

}
answers
@Test

    fun testAnswer() {

        mockkStatic(Log::class)

        every { Log.d(any(), any()) } returns 0

        val mockk = spyk<MockkClassSub>()

//        val mockkClassSub = MockkClassSub()

//        //返回mockkClassSub对象被spyk后的对象,原对象不会改变

//        val spyk1 = spyk(mockkClassSub)

        every { mockk.doSomething1(any()) } answers {

            println("定制mock行为")

            //拿到真实函数信息

            val originalMethod = invocation.originalCall

            //拿到第一个输入参数

            val firstArg = firstArg<String>()

            println("定制mock行为 firstArg $firstArg")



            //调用真实行为并拿到响应结果

            val originalResult = callOriginal()

            //同上

            val originalResult1 = originalMethod.invoke()



            //返回一个固定结果

            "mock result"

        }



        val mockkClass = MockkClass(

musicRepositoryImpl

musicRepositoryImpl)

        //调用被mock的方法

        mockkClass.testDoSomething1("testDoSomething1")

        //调用真实的方法

        mockkClass.testDoSomething2("testDoSomething2")

    }
verify /verifySequence /verify(timeout)

验证多个方法被调用/验证顺序调用/延迟验证

@Test

    fun testVerify() {

        mockkStatic(Log::class)

        every { Log.d(any(), any()) } returns 0

        val mockk = spyk<MockkClassSub>()

//        val mockkClassSub = MockkClassSub()

//        //返回mockkClassSub对象被spyk后的对象,原对象不会改变

//        val spyk1 = spyk(mockkClassSub)

        every { mockk.doSomething1(any()) } returns Unit



        val mockkClass = MockkClass(mockk)

        //调用被mock的方法

        mockkClass.testDoSomething1("")

        //调用真实的方法

        mockkClass.testDoSomething2("")

        verify {

            println("verify")

            mockk.doSomething1("")

            mockk.doSomething2("")

        }

        verifySequence {

            println("verifySequence")

            mockk.doSomething1("")

            mockk.doSomething2("")

        }

        verify(timeout = 2000) {

            println("verify timeout")

            mockk.doSomething1(any())

        }



    }
andthen/andthenThrow/AndThenAnswer

定义后续执行行为

val spyk = spyk<ServiceImplA>()

//定义函数mock行为

val functionAnswer = FunctionAnswer {

    println("functionAnswer")

    ""

}

//定义异常mock行为,返回一个运行时异常

val throwingAnswer = ThrowingAnswer(RuntimeException())

//定义多个行为处理集合,按添加顺序触发

val manyAnswersAnswer = ManyAnswersAnswer(listOf(functionAnswer, throwingAnswer))

//mock

every { spyk.doSomething2(any()) } returns "" andThenAnswer (functionAnswer

        ) andThenAnswer (throwingAnswer

        ) andThenAnswer (manyAnswersAnswer

        //构造了两个ConstantAnswer组成一个ManyAnswersAnswer对象

        //listOf里输入的每个元素会最终赋值到ConstantAnswer的answer方法调用上

        //如果这里传入的是字符串,则代表这个answer就仅仅是返回这个字符串

        //这里的泛型对应里spyk.doSomething2()的返回参数类型

        ) andThenMany (listOf("functionAnswer", "throwingAnswer"))



//第一次执行进入到returns ""

spyk.doSomething2("1")

//进入functionAnswer

spyk.doSomething2("2")

try {

    //第三次进入throwingAnswer抛出运行时异常

    spyk.doSomething2("3")

} catch (e: RuntimeException) {

    println("第三次执行抛出运行时异常")

}

//进入manyAnswersAnswer中的functionAnswer

spyk.doSomething2("4")



try {

    //进入manyAnswersAnswer中的throwingAnswer

    spyk.doSomething2("5")

} catch (e: RuntimeException) {

    println("第5次执行抛出运行时异常")

}

//将返回functionAnswer

println("第6此调用返回:${spyk.doSomething2("6")}")

spyk.doSomething2("7")
mock****局部变量

有时,我们需要检测或控制局部变量。

fun testPrivateParam() {

        Log.d(TAG, "testPrivateParam")

        val mockClassPrivate = MockkClassSub()

        mockClassPrivate.doSomething1("")

    }

@Test

    fun testPrivateParam() {

        mockkStatic(Log::class)

        every { Log.d(any(), any()) } returns 0

        mockkConstructor(MockkClassSub::class)

//        every { anyConstructed<MockkClassSub>().doSomething1("") } returns Unit

        val mockkClass = MockkClass(MockkClassSub())

        mockkClass.testPrivateParam()

        verify {

            anyConstructed<MockkClassSub>().doSomething1("")

        }

    }
Robolectric

注意:robo暂不支持junit5

第一次运行会下载对应android包,比较慢

引包

android {

    testOptions {

        unitTests {

            includeAndroidResources = true

        }

    }

}

//robolectric

    testImplementation 'org.robolectric:robolectric:4.8'
broadcastreceiver

广播的测试点可以包含两个方面

  1. 验证应用程序是否注册了该广播
  2. 验证广播接收器的处理逻辑是否正确,关于逻辑是否正确,可以直接人为的触发onReceive()方法,让然后进行验证
public class MyReceiver extends BroadcastReceiver {

    private final static String TAG = "MyReceiver";



    @Override

    public void onReceive(Context context, Intent intent) {

        Log.d(TAG, "onReceive");

        SharedPreferences.Editor editor = context.getSharedPreferences(

                "account", Context.MODE_PRIVATE).edit();

        String name = intent.getStringExtra("EXTRA_USERNAME");

        editor.putString("USERNAME", name);

        editor.apply();

    }

}
@RunWith(RobolectricTestRunner::class)

class MyReceiverTest {

    @Test

    fun onReceive() {

        val action = "com.max.unitdemo.receiver"

        val intent = Intent(action)

        intent.putExtra("EXTRA_USERNAME", "hello")

        val myReceiver = MyReceiver()

        myReceiver.onReceive(RuntimeEnvironment.application, intent)

        val application = RuntimeEnvironment.application

        val preferences: SharedPreferences = application

            .getSharedPreferences("account", Context.MODE_PRIVATE) as SharedPreferences

        Assert.assertEquals("hello", preferences.getString("USERNAME", ""))

    }



    @Test

    fun register() {

        val shadowApplication = ShadowApplication.getInstance()

        val action = "com.max.unitdemo.receiver"

        val intent = Intent(action)



        // 验证是否注册了相应的Receiver

        Assertions.assertTrue(shadowApplication.hasReceiverForIntent(intent))

    }

}
Activity

robo提供了ActivityController,通过ActivityController,我们可以很容易操作activity对应生命周期事件

详细使用见demo

Fragment

fragment的测试在robo中已经废弃,官方推荐使用FragmentScenario来测试

主要功能包括创建fragment,操作生命周期,拿到fragment的对象。可以与robo一起使用

android 引包

//fragment test

debugImplementation("androidx.fragment:fragment-testing:1.5.4")
Service

robo提供了ServiceController操作service对应生命周期

Toast
binding.buttonActivity.setOnClickListener {

    Toast.makeText(this@MainActivity, "hello", Toast.LENGTH_LONG).show()

}
@Test

    fun testToast() {

        val activity = Robolectric.setupActivity(MainActivity::class.java)

        val btn = activity.findViewById<View>(R.id.button_activity) as Button

        btn.performClick()

//        val shadowActivity = Shadows.shadowOf(activity)

//        val shadowTextView = Shadows.shadowOf(btn)

        Assert.assertNotNull(ShadowToast.getLatestToast())

        Assert.assertEquals("hello", ShadowToast.getTextOfLatestToast())

    }
Shadow

robolectric有很多shadow类来修改或拓展Android OS原本的类......每一次执行android类时,robolectric确保shadow类先执行。再详细一点作用,android.jar只是让我们app能编译通过,运行时如果使用android.jar则会报RuntimeException,对应的Shadow类就屏蔽掉了这个问题,同时Shadow类也提供了对应的记录功能,方便测试和拿到对应的状态。如ShadowActivity,ShadowService,ShadowToast。

android原生的类基本robo都有Shadow,同时,robo也提供了自定义Shadow类的功能。

class Company(var name: String?) {



    fun welcome() {

        println("method called in Company.")

    }



    fun sayHello() {

        println("say hello in Company.")

    }

}
@Implements(Company::class)

class ShadowCompany {

    //Robolectric会自动设置真实的关联对象

    //注意,如果调用使用被shadow的函数,还是会走到shadow类中

    @RealObject

    private val company: Company? = null



    //必须实现该方法,参数与构造函数参数一样

    fun __constructor__(name: String?) {

        println("constructor in shadow class. ")

    }



    @Implementation

    fun welcome() {

//        company?.welcome()

        println("method called in ShadowCompany. ${company?.name}")

    }

}
//通过shadows配置自定义的shadow class

@Config(shadows = [ShadowCompany::class])

@Test

fun testShadow() {

    val company = Company("hello")

    company.welcome()

    company.sayHello()

}

注意

  1. @Implements注解指定需要对哪个类进行shadow;
  2. @Implementation指定需要对哪个方法进行替换;
  3. 使用__constructor__来对构造器进行替换;
  4. @RealObject来引用真实的关联对象;

仪器化测试

在AndroidTest目录下,使用AndroidJunit4。运行测试用例时,会直接将app跑到真机上,在运行时就可以拿到app的各个对象。

常用的测试框架有 UiAutomator,Espressso

UiAutomator:通过匹配规则找到控件,然后调用相关事件。1.0版本是shell进程,2.0版本是app进程(可以使用targetapp数据)。两个版本都是通过 系统的 UiAutomator类来发送相关事件

testerhome.com/articles/20…

Espressso: 通过匹配规则找到控件,调用相关事件。8.0以上版本才支持跨进程

Android Espresso是如何获取View? (一)—— Espresso源码篇 - 掘金

这两个框架常用于UI自动化测试,不适用于做函数的单元测试。

测试范围

框架只是便于我们测试代码,对于项目不同层,可以不使用或使用不同的测试框架

以上面结构举例

activity/receiver/service:robo + mockk

fragment:robo + mockk

viewmodel: mockk

repository: mockk

database:robo + mockk

network: mockk

覆盖率

测试结果

jacoco覆盖率

jacoco引包

plugins {

    id 'jacoco'

}

Demo

暂时无法在文档外展示此内容

测试心得

考虑可读性:对于方法名使用表达能力强的方法名,对于测试范式可以考虑使用一种规范, 如 RSpec-style。方法名可以采用一种格式,如:测试的方法_条件__预期结果。

不要使用逻辑流关键字:比如(If/else、for、do/while、switch/case),在一个测试方法中,如果需要有这些,拆分到单独的每个测试方法里。

测试真正需要测试的内容:需要覆盖的情况,一般情况只考虑验证输出(如某操作后,显示什么,值是什么)。

不需要考虑测试private的方法:将private方法当做黑盒内部组件,测试对其引用的public方法即可;不考虑测试琐碎的代码,如getter或者setter。

每个单元测试方法,应没有先后顺序:尽可能的解耦对于不同的测试方法,不应该存在Test A与Test B存在时序性的情况。

引用

Android—junit5

blog.csdn.net/yihanss/art…

junit5 官方文档

junit.org/junit5/docs…

gitee.com/liushide/ju…

mockito

github.com/mockito/moc…

javadoc.io/static/org.…

mockk

mockk.io/#private-fu…

Robolectric使用教程

www.cnblogs.com/hanschen-co…

developer.android.google.cn/reference/k…