单元测试是应用程序测试策略中的基本测试,通过对代码进行单元测试,可以轻松地验证单个单元的逻辑是否正确,在每次构建之后运行单元测试,可以帮助您快速捕获和修复因代码更改(重构、优化等)带来的回归问题。
为什么要进行单元测试
提高稳定性,能够明确地了解是否正确的完成开发;
快速反馈bug,跑一遍单元测试用例,定位bug;
在开发周期中尽早通过单元测试检查bug,最小化技术债,越往后可能修复bug的代价会越大,严重的情况下会影响项目进度;
为代码重构提供安全保障,在优化代码时不用担心回归问题,在重构后跑一遍测试用例,没通过说明重构可能是有问题的,更加易于维护。
单元测试要测什么?
列出想要测试覆盖的正常、异常情况,进行测试验证;
性能测试,例如某个算法的耗时等等。
1.本地测试(Local tests): module-name/src/test/
只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的Mockito;
优点:运行速度快、效率高。
缺点:一般情况下,测试代码不能有Android依赖。若有Android依赖且要使用本地测试的方式,可以使用测试框架如Mockito来实现。
2.仪器化测试(Instrumented tests): module-name/src/androidTest/
在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般的,依赖不太方便通过模拟框架模拟时采用这种方式。
优点:测试代码直接支持对Android的依赖。
缺点:需要真机或模拟器配合,运行速度较本地测试稍慢。实际上,在这类测试过程中是编译了一个额外的Apk,并安装到手机或模拟器中运行的。
测试框架选择
目前流行的Android测试框架较多,按照对Android依赖的强弱情况,可以分为:
无依赖:JUnit
弱依赖:Mockito、 AndroidJUnitRunner
强依赖:Espresso
本地测试(Local tests):配置
dependencies { testImplementation 'junit:junit:4.12' // Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果) testImplementation 'org.mockito:mockito-core:3.5.13' }
仪器化测试(Instrumented tests):配置(AndroidX)
dependencies { androidTestImplementation 'com.android.support:support-annotations:28.0.0' androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0' }
android { ... defaultConfig { ... testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } }
@Test
public void method()
定义所在方法为单元测试方法
@Test (expected = Exception.class)
public void method()
测试方法若没有抛出Annotation中的Exception类型(子类也可以)->失败
@Test(timeout=100)
public void method()
性能测试,如果方法耗时超过100毫秒->失败
@Before
public void method()
这个方法在每个测试之前执行,用于准备测试环境(如: 初始化类,读输入流等),在一个测试类中,每个@Test方法的执行都会触发一次调用。
**@After **
public void method()
这个方法在每个测试之后执行,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用。
@BeforeClass
public static void method()
这个方法在所有测试开始之前执行一次,用于做一些耗时的初始化工作(如: 连接数据库),方法必须是static
@AfterClass
public static void method()
这个方法在所有测试结束之后执行一次,用于清理数据(如: 断开数据连接),方法必须是static
@Ignore或者@Ignore(“太耗时”)
public void method()
忽略当前测试方法,一般用于测试方法还没有准备好,或者太耗时之类的
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestClass{}
使得该测试类中的所有测试方法都按照方法名的字母顺序执行,可以指定3个值,分别是DEFAULT、JVM、NAME_ASCENDING
单元测试代码存储位置
事实上,AS已经帮我们创建好了测试代码存储目录
app/src ├── androidTestjava (仪器化单元测试、UI测试) ├── main/java (业务代码) └── test/java (本地单元测试)
创建测试类
可以自己手动在相应目录创建测试类,AS也提供了一种快捷方式:选择对应的类->将光标停留在类名上->按下ALT + ENTER->在弹出的弹窗中选择Create Test
运行测试用例
运行单个测试方法:选中@Test注解或者方法名,右键选择Run;
运行一个测试类中的所有测试方法:打开类文件,在类的范围内右键选择Run,或者直接选择类文件直接右键Run;
运行一个目录下的所有测试类:选择这个目录,右键Run。
(1)本地测试(Local tests):
业务类
public class WeekTestActivity extends Activity { public int findKthLargest(int[] nums, int k) { for (int i = 0; i < nums.length - 1; i++) { for (int j = 0; j < nums.length - 1; j++) { if (nums[j] < nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; } } } return nums[k - 1]; } }
测试类
public class WeekTestActivityTest { @Test public void findKthLargestTest() { WeekTestActivity weekTestActivity = new WeekTestActivity(); int arr[] = {26, 15, 29, 66, 99, 88, 36, 77, 111, 1, 6, 8, 8}; int k = 4; int result = weekTestActivity.findKthLargest(arr, k); assertEquals(77, result); } }
运行结果
这里返回code 0 是测试通过
修改预测结果
assertEquals(66, result);
报结果错误
在单元测试中通过System.out或者System.err打印的也会输出。
(2)仪器化测试
在某些情况下,虽然可以通过模拟的手段来隔离Android依赖,但代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量。
仪器化测试是在真机或模拟器上运行的测试,它们可以利用Android framework APIs 和 supporting APIs。如果测试用例需要访问仪器(instrumentation)信息(如应用程序的Context),或者需要Android框架组件的真正实现(如Parcelable或SharedPreferences对象),那么应该创建仪器化单元测试,由于要跑到真机或模拟器上,所以会慢一些。
业务类
public class SharedPreferenceDao { private SharedPreferences sp; public SharedPreferenceDao(SharedPreferences sp) { this.sp = sp; } public SharedPreferenceDao(Context context) { this(context.getSharedPreferences("config", Context.MODE_PRIVATE)); } public void put(String key, String value) { SharedPreferences.Editor editor = sp.edit(); editor.putString(key, value); editor.apply(); } public String get(String key) { return sp.getString(key, null); } }
测试类
// @RunWith 只在混合使用 JUnit3 和 JUnit4 需要,若只使用JUnit4,可省略 @RunWith(AndroidJUnit4.class) public class SharedPreferenceDaoTest { public static final String TEST_KEY = "express"; public static final String TEST_STRING = "快递"; SharedPreferenceDao spDao; @Before public void setUp() { spDao = new SharedPreferenceDao(App.getContext()); } @Test public void sharedPreferenceDaoWriteRead() { spDao.put(TEST_KEY, TEST_STRING); Assert.assertEquals(TEST_STRING, spDao.get(TEST_KEY)); } }
运行结果
Testing started at 18:01 ... 09/29 18:01:46: Launching 'sharedPreferenceDa...()' on Xiaomi Redmi 7. Running tests $ adb shell am instrument -w -r -e debug false -e class 'com.app.wangxu.testapplication.SharedPreferenceDaoTest#sharedPreferenceDaoWriteRead' com.app.wangxu.testapplication.test/androidx.test.runner.AndroidJUnitRunner Connected to process 16726 on device 'xiaomi-redmi_7-693cd27a'. Started running tests Tests ran to completion.
Mock的概念,其实很简单,我们前面也介绍过:所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:
验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作
JUnit 是单元测试框架。Mockito 与 JUnit 不同,并不是单元测试框架(这方面 JUnit 已经足够好了),它是用于生成模拟对象或者直接点说,就是”假对象“的工具。两者定位不同,所以一般通常的做法就是联合 JUnit + Mockito 来进行测试。
使用时在build文件中添加依赖
dependencies { // Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果) testImplementation 'org.mockito:mockito-core:3.5.13' }
1.四种Mock方式
普通方法:
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); } }
其中后两种方法是结合JUnit框架去实现的
2.常用打桩方法
因为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方法什么也不做 |
3.常用验证方法
前面所说的都是状态测试,但是如果不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。 从概念上讲,就是和状态测试所不同的“行为测试”了。
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) |
@RunWith(MockitoJUnitRunner.class) public class MockitoTest { @Mock private ArrayList mockList; @Mock private Person mPerson; @Test public void testIsNotNull() { Assert.assertNotNull(mockList); } @Test public void sampleTest() throws Exception { Mockito.doAnswer(invocation -> { System.out.println("测试无返回值的函数"); return null; }).when(mockList).clear(); mockList.add("sampleTest"); mockList.clear(); Mockito.verify(mockList).add("sampleTest"); Mockito.verify(mockList).clear(); } @Test public void testPersonAnswer() { Mockito.when(mPerson.getName()).thenReturn("小明"); Mockito.when(mPerson.getKey()).thenReturn("www.baidu.com"); System.out.print(mPerson.getName()); System.out.print(mPerson.getKey()); } }
4.其他方法
方法名 | 方法描述 |
reset(T … mocks) | 重置Mock |
spy(Class classToSpy) | 实现调用真实对象的实现 |
inOrder(Object… mocks) | 验证执行顺序 |
@InjectMocks注解 | 自动将模拟对象注入到被测试对象中 |
@Spy public class MockitoSpyTest { @Spy Person mPerson; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Test public void testIsNotNull(){ assertNotNull(mPerson); } @Test public void testPersonSpy(){ //输出11 System.out.print(mPerson.getAge()); } }
2.inOrder使用代码及测试结果:
3.@InjectMocks: 创建一个实例,这个实例需要的参数用@Mock(或@Spy)注解创建的注入到该实例中。
public class Home { private Person mPerson; public Home(Person person) { mPerson = person; } public String getMaster(){ return mPerson.getName(); } }
其他:Mockito框架不支持mock匿名类、final类、static方法、private方法
作者:Sunny_Snail
原文链接:https://blog.csdn.net/qq_32324617/article/details/108826874