单元测试
🧨

单元测试

1、kotlin

2、Mockito 基础及使用

 

1. 什么是单元测试

定义

单元测试是一种软件测试方法,通过对软件中的最小可测试部分(通常是一个函数或方法)进行验证,确保其功能正确性。单元测试通常由开发者编写,以快速验证代码在修改后的行为是否符合预期。

目的

● 验证代码正确性:确保每个单元在各种情况下都能按预期工作。
● 快速反馈:在代码修改后快速检测引入的错误。
● 文档化:提供代码用法示例,辅助理解和维护。

特点

● 独立性:单元测试应独立于其他测试,不依赖外部资源(如数据库、网络)。
● 自动化:能够自动执行,通常集成到持续集成系统中。
● 快速执行:测试运行速度快,便于频繁执行。

工程配置

添加依赖
// JUnit
testImplementation 'junit:junit:4.13.2'

2. 介绍Mockito的由来

Mockito是由Szczepan Faber在2008年创建的一个Java测试框架,用于简化测试中的模拟(Mocking)过程。它基于EasyMock的理念,致力于提供一种更简单、更直观的方式来编写和管理测试代码。Mockito允许开发者创建和配置模拟对象,以便隔离单元测试中的依赖,专注于测试逻辑的核心部分。

3. 为什么要使用Mockito,解决哪些测试痛点

测试痛点

● 依赖过多:当一个类依赖许多其他类时,测试它变得复杂,因为需要处理所有依赖的初始化和状态。
● 难以控制的外部依赖:某些依赖可能涉及到数据库、网络请求等外部资源,这些资源在测试环境中难以控制和配置。
● 复杂的交互:类之间的交互复杂时,难以验证所有交互的正确性。

Mockito的优势

● 隔离依赖:通过创建模拟对象,可以隔离被测试对象的依赖,只关注其逻辑。
● 简化测试代码:提供直观的API,减少测试代码的复杂性。
● 控制行为:可以精确控制模拟对象的行为,确保测试的可预测性和稳定性。
● 验证交互:允许验证模拟对象的交互,确保依赖关系和调用顺序正确。

4. 哪些地方可以使用,以及如何使用

Mockito可以用于任何涉及到对象依赖的单元测试场景,特别适合以下情况:
● 测试需要依赖多个其他对象的类。
● 测试依赖外部资源的类(如数据库、Web服务)。
● 测试复杂的对象交互。
依赖配置
dependencies {     //Mockito for unit tests     testImplementation "org.mockito:mockito-core:2.+"     //Mockito for Android tests     androidTestImplementation 'org.mockito:mockito-android:2.+' }
● mockito-core: 用于 本地单元测试,其测试代码路径位于 module-name/src/test/java/
● mockito-android: 用于 设备测试,即需要运行 android 设备进行测试,其测试代码路径位于 module-name/src/androidTest/java/

5. 相关概念

● Mock 对象
mock对象的概念就是我们想要创建一个可以替代实际对象的对象,这个模拟对象要可以通过特定参数调用特定的方法,并且能返回预期结果。
● Stub(桩)
桩指的是用来替换具体功能的程序段。桩程序可以用来模拟已有程序的行为或是对未完成开发程序的一种临时替代。
● 设置预期
通过设置预期明确 Mock对象执行时会发生什么:
比如返回特定的值、抛出一个异常、触发一个事件等,又或者调用一定的次数
● 验证预期的结果
设置预期和验证预期是同时进行的。设置预期在调用测试类的函数之前完成,验证预期则在它之后。
所以,首先你设定好预期结果,然后去验证你的预期结果是否正确

6. 关键字介绍

when

when关键字用于指定要模拟的方法调用。

thenReturn

thenReturn关键字用于指定当模拟的方法被调用时应该返回的值。

thenThrow

thenThrow关键字用于指定当模拟的方法被调用时应该抛出的异常。

thenAnswer

thenAnswer关键字用于指定当模拟的方法被调用时应该执行的自定义行为。
使用Mockito设置预期行为
import org.junit.Test; import org.mockito.Mockito; import static org.mockito.Mockito.*; import static org.junit.Assert.*; public class CalculatorTest {     @Test     public void testCalculator() {         // 创建Mock对象         Calculator calculator = mock(Calculator.class);         // 设置预期行为:when 和 thenReturn         when(calculator.add(10, 20)).thenReturn(30);         when(calculator.subtract(20, 10)).thenReturn(10);         // 设置预期行为:thenThrow         when(calculator.divide(10, 0)).thenThrow(new IllegalArgumentException("Divider cannot be zero"));         // 设置预期行为:thenAnswer         when(calculator.multiply(anyInt(), anyInt())).thenAnswer(invocation -> {             Object[] args = invocation.getArguments();             return (int) args[0] * (int) args[1];         });         // 验证预期行为         assertEquals(30, calculator.add(10, 20));         assertEquals(10, calculator.subtract(20, 10));         try {             calculator.divide(10, 0);             fail("Expected IllegalArgumentException");         } catch (IllegalArgumentException e) {             assertEquals("Divider cannot be zero", e.getMessage());         }         assertEquals(200, calculator.multiply(10, 20));     } }

7. 验证方法调用的关键字:verify

用法

verify用于验证模拟对象的方法是否按预期被调用。它可以检查方法的调用次数、调用顺序、参数等。
import static org.mockito.Mockito.*; import org.junit.Test; public class CalculatorTest {     @Test     public void testVerify() {         // 创建Mock对象         Calculator calculator = mock(Calculator.class);         // 调用方法         calculator.add(10, 20);         calculator.subtract(20, 10);         // 验证方法被调用         verify(calculator).add(10, 20);         verify(calculator).subtract(20, 10);         // 验证方法调用次数         verify(calculator, times(1)).add(10, 20);         verify(calculator, times(1)).subtract(20, 10);     } }

关键方法

● verify(T mock): 验证方法是否被调用。
● times(int wantedNumberOfInvocations): 验证方法被调用的次数。
● never(): 验证方法从未被调用。
● atLeast(int minNumberOfInvocations): 验证方法至少被调用的次数。
● atMost(int maxNumberOfInvocations): 验证方法最多被调用的次数。
● inOrder(T... mocks): 验证方法的调用顺序。

其他用法

你还可以使用argumentCaptor捕获参数,验证方法调用时传递的参数。
import static org.mockito.Mockito.*; import org.mockito.ArgumentCaptor; import org.junit.Test; import static org.junit.Assert.*; public class CalculatorTest {     @Test     public void testArgumentCaptor() {         // 创建Mock对象         Calculator calculator = mock(Calculator.class);         // 调用方法         calculator.add(10, 20);         // 创建ArgumentCaptor         ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);         // 捕获参数         verify(calculator).add(captor.capture(), captor.capture());         // 验证参数         assertEquals((Integer) 10, captor.getAllValues().get(0));         assertEquals((Integer) 20, captor.getAllValues().get(1));     } }

8. 主要API使用

创建Mock对象
List mockedList = mock(List.class);
定义行为
when(mockedList.get(0)).thenReturn("first"); when(mockedList.get(anyInt())).thenThrow(new RuntimeException());
验证行为
verify(mockedList).add("one"); verify(mockedList, times(1)).add("one");
其他API
● 捕获参数:ArgumentCaptor<T> captor = ArgumentCaptor.forClass(Class<T>)
● 部分模拟:Spy<T> spy(T object)
● 序列化:Serialized<T> serialized = Serialized<T>(T object)
示例代码
import org.junit.Test; import static org.mockito.Mockito.*; public class MockitoTest {     @Test     public void testMockito() {         // 创建Mock对象         List<String> mockedList = mock(List.class);         // 定义Mock行为         when(mockedList.get(0)).thenReturn("first");         // 调用被测试代码         String result = mockedList.get(0);         // 验证结果         assertEquals("first", result);         // 验证交互         verify(mockedList).get(0);     } }

Mockito Spy

在Mockito中,Spy用于部分模拟对象。与Mock对象不同,Spy对象是一个真实对象的代理,它允许你监控对象的行为,并在必要时改变其行为。Spy对象可以调用真实对象的方法,同时也可以模拟部分方法的行为。

主要特点

● 真实方法调用:默认情况下,Spy对象会调用真实对象的方法。
● 部分模拟:可以选择性地模拟对象的方法,而不是完全替换。
● 监控行为:可以验证方法调用和参数。

使用场景

Spy通常用于以下场景:
● 需要测试对象的部分行为,但又需要调用其真实方法。
● 需要监控对象的方法调用,但不希望完全模拟整个对象。

示例代码

假设我们有一个简单的类Calculator,我们希望测试其中的部分方法,同时调用其他方法的真实实现。

创建Calculator类

public class Calculator {     public int add(int a, int b) {         return a + b;     }     public int subtract(int a, int b) {         return a - b;     }     public int multiply(int a, int b) {         return a * b;     } }
 

使用Spy进行测试

我们希望在测试中调用add方法的真实实现,但模拟subtract方法的行为。
import org.junit.Test; import org.mockito.Mockito; import org.mockito.Spy; import static org.mockito.Mockito.*; import static org.junit.Assert.*; public class CalculatorTest { @Mock Calculator cc1     @Spy     Calculator calculator = new Calculator();     @Test     public void testAddAndSubtract() {         // 使用MockitoAnnotations初始化Spy对象         MockitoAnnotations.initMocks(this);         // 调用真实的add方法         int addResult = calculator.add(10, 20);         assertEquals(30, addResult);         // 模拟subtract方法的行为         doReturn(100).when(calculator).subtract(20, 10);         // 调用subtract方法,验证模拟行为         int subtractResult = calculator.subtract(20, 10);         assertEquals(100, subtractResult);         // 验证add方法的真实调用         verify(calculator).add(10, 20);         // 验证subtract方法的模拟调用         verify(calculator).subtract(20, 10);     } }

详细说明

创建Spy对象:通过@Spy注解或Mockito.spy()方法创建Spy对象。在这里,我们使用注解方式。
@Spy Calculator calculator = new Calculator();
初始化Spy对象:使用MockitoAnnotations.initMocks(this)方法初始化注解。
MockitoAnnotations.initMocks(this);
调用真实方法:调用Spy对象的add方法,该方法会调用真实的实现。
doReturn(100).when(calculator).subtract(20, 10);
调用并验证模拟方法:调用subtract方法并验证返回值,同时验证方法的调用情况。
int subtractResult = calculator.subtract(20, 10); assertEquals(100, subtractResult); verify(calculator).subtract(20, 10);
通过使用Spy,你可以在不完全模拟整个对象的情况下,部分控制对象的行为,这在某些测试场景中非常有用。
google demo
Mockito官方demo

3、Robolectric 基础与使用

1. robolectric是什么?

官方原文:
Robolectric is the industry-standard unit testing framework for Android. With Robolectric, your tests run in a simulated Android environment inside a JVM, without the overhead and flakiness of an emulator. Robolectric tests routinely run 10x faster than those on cold-started emulators.
Robolectric supports running unit tests for 14 different versions of Android, ranging from Lollipop (API level 21) to U (API level 34).
译:
Robolectric 是 Android 的行业标准单元测试框架。借助 Robolectric,您的测试可以在 JVM 内的模拟 Android 环境中运行,而无需模拟器的开销和不稳定。 Robolectric 测试的运行速度通常比冷启动模拟器上的测试快 10 倍。

2. Robolectric能帮助解决什么问题?

测试Android代码逻辑,光有JUnit和Mockito是不够的,假设你使用了TextView的setText,用Mockito框架的话,默认的TextView的getText方法会返回null,如果是简单的代码,使用Mockito的桩设置还可以接受,如果是要测试到Activity的生命周期等一些复杂逻辑就显得比较复杂了。
为了解决这个问题,诞生了Instrumentation、Robolectric等等的测试框架,不过Instrumentation实际上还是要运行代码到平台上测试,耗费大量的时间,介绍的是运行在JVM上的Robolectric测试框架。

3. Robolectric基本原理

在使用Robolectric之前我们先要明白Robolectric是如何工作的。比如说我们前文说到的TextView,如果我们使用Mockito,他给我们提供的是Mock后的TextView,而Robolectric给我们提供的是ShadowTextView,这个ShadowTextView实现了TextView身上的方法,但他又与Android的运行环境无关,也就是说他可以像使用TextView一样的方法,但不用在平台上运行代码,大大提高测试效率。

4. 我们为什么需要使用Robolectric?

为解决在JVM中能正常使用Android Context及其支持的API,如获取app的真实渠道名,获取app需要加载的资源文件,如assert目录下的资源图,或者文件,以及被测试类可能依赖Context进行其他接口调用或者参数传递。

5. 什么是Android Context?为什么必须使用它呢?

我们最常用的Activity,Service,Application都是Context的子类。所以知道Context的具体实现是非常有必要的。
下面是Context的体系结构图:
notion image
Context是Android App程序的基础,可以看作java 的 man()函数一样的重要,Android很多的API,功能都是需要在Context初始化后才能进行的。

6. 怎么配置使用呢?

build.gradle:
testImplementation "junit:junit:4.13.2" testImplementation "org.robolectric:robolectric:4.12.2"
测试代码中使用示例:
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class) public class MainActivityTest { }

7. 配置SDK版本

Robolectric会根据manifest文件配置的targetSdkVersion选择运行测试代码的SDK版本,如果你想指定sdk来运行测试用例,可以通过下面的方式配置
@Config(sdk = Build.VERSION_CODES.JELLY_BEAN) public class SandwichTest {     @Config(sdk = Build.VERSION_CODES.KITKAT)     public void getSandwich_shouldReturnHamSandwich() {     } }

8. 配置Application类

Robolectric会根据manifest文件配置的Application配置去实例化一个Application类,如果你想在测试用例中重新指定,可以通过下面的方式配置
@Config(application = CustomApplication.class) public class SandwichTest {     @Config(application = CustomApplicationOverride.class)     public void getSandwich_shouldReturnHamSandwich() {     } }

9. 驱动Activity生命周期

利用ActivityController我们可以让Activity执行相应的生命周期方法,如
    @Test     public void testLifecycle() throws Exception {         // 创建Activity控制器         ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);         MainActivity activity = controller.get();         assertNull(activity.getLifecycleState());         // 调用Activity的performCreate方法         controller.create();         assertEquals("onCreate", activity.getLifecycleState());         // 调用Activity的performStart方法         controller.start();         assertEquals("onStart", activity.getLifecycleState());         // 调用Activity的performResume方法         controller.resume();         assertEquals("onResume", activity.getLifecycleState());         // 调用Activity的performPause方法         controller.pause();         assertEquals("onPause", activity.getLifecycleState());         // 调用Activity的performStop方法         controller.stop();         assertEquals("onStop", activity.getLifecycleState());         // 调用Activity的performRestart方法         controller.restart();         // 注意此处应该是onStart,因为performRestart不仅会调用restart,还会调用onStart         assertEquals("onStart", activity.getLifecycleState());         // 调用Activity的performDestroy方法         controller.destroy();         assertEquals("onDestroy", activity.getLifecycleState());     }
UI组件状态
@Test public void testViewState(){      CheckBox checkBox = (CheckBox) sampleActivity.findViewById(R.id.checkbox);      Button inverseBtn = (Button) sampleActivity.findViewById(R.id.btn_inverse);      assertTrue(inverseBtn.isEnabled());      checkBox.setChecked(true);      //点击按钮,CheckBox反选      inverseBtn.performClick();      assertTrue(!checkBox.isChecked());      inverseBtn.performClick();      assertTrue(checkBox.isChecked());  } Dialog @Test public void testDialog(){      //点击按钮,出现对话框      dialogBtn.performClick();      AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();      assertNotNull(latestAlertDialog);  } Toast @Test public void testToast(){      //点击按钮,出现吐司      toastBtn.performClick();      assertEquals(ShadowToast.getTextOfLatestToast(),"we love UT");  }

10. 其他支持

Fragment的测试
BroadcastReceiver的测试
Service的测试

11. 什么是Shadow类

Shadow是Robolectric的立足之本,如其名,作为影子,一定是变幻莫测,时有时无,且依存于本尊。Robolectric定义了大量模拟Android系统类行为的Shadow类,当这些系统类被创建的时候,Robolectric会查找对应的Shadow类并创建一个Shadow类与原始类关联。每当系统类的方法被调用的时候,Robolectric会保证Shadow对应的方法会调用。这些Shadow对象,丰富了本尊的行为,能更方便的对Android相关的对象进行测试。
比如,我们可以借助ShadowActivity验证页面是否正确跳转了
/** * 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面 * * @throws Exception */ @Test public void testJump() throws Exception {         // 默认会调用Activity的生命周期: onCreate->onStart->onResume         MainActivity activity = Robolectric.setupActivity(MainActivity.class);         // 触发按钮点击         activity.findViewById(R.id.activity_main_jump).performClick();         // 获取对应的Shadow类         ShadowActivity shadowActivity = Shadows.shadowOf(activity);         // 借助Shadow类获取启动下一Activity的Intent         Intent nextIntent = shadowActivity.getNextStartedActivity();         // 校验Intent的正确性         assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());     }

12. 如何自定义Shadow对象

首先,创建原始对象Person
public class Person {     private String name;     public Person(String name) {         this.name = name;     }     public String getName() {         return name;     } }
其次,创建Person的Shadow对象
@Implements(Person.class) public class ShadowPerson {     @Implementation     public String getName() {         return "geniusmart";     } }
接下来,需自定义TestRunner,添加Person对象为要进行Shadow的对象
public class CustomShadowTestRunner extends RobolectricGradleTestRunner {     public CustomShadowTestRunner(Class<?> klass) throws InitializationError {         super(klass);     }     @Override     public InstrumentationConfiguration createClassLoaderConfig() {         InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder();         /**          * 添加要进行Shadow的对象          */         builder.addInstrumentedClass(Person.class.getName());         return builder.build();     } }
最后,在测试用例中,ShadowPerson对象将自动代替原始对象,调用Shadow对象的数据和行为
@RunWith(CustomShadowTestRunner.class) @Config(constants = BuildConfig.class,shadows = {ShadowPerson.class}) public class ShadowTest {     /**      * 测试自定义的Shadow      */     @Test     public void testCustomShadow(){         Person person = new Person("genius");         //getName()实际上调用的是ShadowPerson的方法         assertEquals("geniusmart", person.getName());         //获取Person对象对应的Shadow对象         ShadowPerson shadowPerson = (ShadowPerson) ShadowExtractor.extract(person);         assertEquals("geniusmart", shadowPerson.getName());     } }

13. 追加模块

为了减少依赖包的大小,Robolectric的shadows类成了好几部分:
SDK Package
Robolectric Add-On Package
com.android.support.support-v4
org.robolectric:shadows-support-v4
com.android.support.multidex
org.robolectric:shadows-multidex
com.google.android.gms:play-services
org.robolectric:shadows-play-services
com.google.android.maps:maps
org.robolectric:shadows-maps
org.apache.httpcomponents:httpclient
org.robolectric:shadows-httpclient

其他

Android单元测试学习资料

● Android官方文档,目前我们主要采用本地化测试方案

● Android 单元测试从入门到放弃

主要介绍本地测试使用的技术框架,依赖库的优缺点:

Gradle For Android(6)--测试单元

介绍AndroidStuido中如何使用单元测试相关工具

● mockito框架使用总结

介绍mocktio的APi,如何使用

● 2023单元测试利器Mockito框架详解

● PowerMockito使用详解

● Android最佳Mock单元测试方案:Junit + Mockito + Powermock

介绍目前比较实用的测试单元框架的使用

● Robolectric使用教程

可支持Android UI框架的测试库,覆盖范围比mockito,powermockito更宽

● Android 单元测试之Robolectric

框架库原码工程及文档

Mockito 中文文档

MockKotlin

支持kotlin的mockito库,主要是代码写法和格式为kotlin格式

PowerMockito

mockito的加强版,支持的范围比mockito大些

Robolectric