一、什么是单元测试
单元测试是测试某个类的某个方法能否正常工作的一种手段。
单元测试的粒度:一般一个public方法需要一个test case
二、单元测试目的
- 验收(改动和重构)
- 快速验证逻辑
- 优化代码设计
三、单元测试工具
junit4 + mockito + powermock
junit4:JUnit是Java最基础的测试框架,主要的作用就是断言
Mock的作用:解决测试类对其他类的依赖问题。Mock的类所有方法都是空,所有变量都是初始值。
PowerMock:PowerMock是Mockito的扩展增强版,支持mock private、static、final方法和类,还增加了很多反射方法可以方便修改静态和非静态成员等。功能比Mockito增加很多。
- // build.gradle中引入powermock
- testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
- testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
四、单元测试流程
1、新建测试类(快捷导航键: ctrl+shift+T),新建测试用例名
2、setUp 初始化一些公共的东西
3、编写测试代码,执行操作
4、验证结果
一般我们依据被测方法是否有返回值选用不同的验证方法。
有返回值的,直接调用该方法得到返回结果,使用JUnit的Asset验证结果;
没有返回值的,则看方法最终调用了依赖对象的哪个方法,然后再校验依赖对象的该方法有没有被调用,以及获取到的参入参数是否正确
举例说明:
- public void login(String username, String password) {
- if (username == null || username.length() == 0) {
- return;
- }
- if (password == null || password.length() < 6) {
- return;
- }
- mUserManager.performLogin(username, password);
- }
我们要验证该login方法是否正确,则依据传入的参数,判断mUserManager的performLogin方法是否得要了调用。
五、基础用法
常见注解:
- @Before: 如果一个方法被@Before修饰过了,那么在每个测试方法调用之前,这个方法都会得到调用。
- @After: 每个测试方法运行结束之后,会得到运行的方法
- @Test:如果一个方法被@Before修饰过了,那么这个方法为可执行的测试用例,注解设置expected参数 可验证一个方法是否抛出了异常
- @Ignore:忽略的测试方法
- @RunWith 指定该测试类使用某个运行器
- @Rule:重新制定测试类中方法的行为,可以理解为在测试用例执行前和执行后插桩
- @Mock: 创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:
a.验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
b.指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作
注意:mock出来的对象并不会自动替换掉正式代码里面的对象,你必须要有某种方式把mock对象应用到正式代码里面
junit框架中Assert类的常用方法
- assertEquals: 断言传入的预期值与实际值是相等的
- assertNotEquals: 断言传入的预期值与实际值是不相等的
- assertArrayEquals: 断言传入的预期数组与实际数组是相等的
- assertNull: 断言传入的对象是为空
- assertTrue: 断言条件为真
- assertFalse: 断言条件为假
- assertSame: 断言两个对象引用同一个对象,相当于“==”
Mockito的使用
Mockito的使用主要分三步:Mock/spy对象 + 打桩 + 验证
示例:
- when(mockObj.methodName(params)).thenReturn(result)
- mock: 所有方法都是空方法,非void方法都将返回默认值,比如int方法返回0,对象方法将返回null,而void方法将什么都不做。 适用于类对外部依赖较多,只关新少数函数的具体实现;
- spy:跟正常类对象一样,是正常对象的替身。适用场景跟mock相反,类对外依赖较少,关心大部分函数的具体实现。
四种Mock方式:
- 普通方法:
- @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);
- }
- }
常用参数匹配
- anyObject() 匹配任何对象
- any(Class
type) 与anyObject()一样 - any() 与anyObject()一样 (慎用,有些场景会导致测试用例执行失败)
- anyBoolean() 匹配任何boolean和非空Boolean
- anyByte() 匹配任何byte和非空Byte
- anyInt() 匹配任何int和非空Integer
- anyString() 匹配任何非空String
- …
常用打桩方法
- thenReturn(T value) 设置要返回的值
- thenThrow(Throwable… throwables) 设置要抛出的异常
- thenAnswer(Answer answer) 对结果进行拦截
- doReturn(Object toBeReturned) 提前设置要返回的值
- doThrow(Throwable… toBeThrown) 提前设置要抛出的异常
- doAnswer(Answer answer) 提前对结果进行拦截
- doCallRealMethod() 调用某一个方法的真实实现
- doNothing() 设置void方法什么也不做
PowerMock使用
首先使用PowerMock必须加注解@PrepareForTest和@RunWith(PowerMockRunner.class)。注解@PrepareForTest里写的是静态方法所在的类,如果@RunWith被占用。这时我们可以使用@Rule来解决
- @Rule
- public PowerMockRule rule = new PowerMockRule();
- mock静态方法
- @RunWith(PowerMockRunner.class)
- public class PowerMockitoStaticMethodTest {
- @Test
- @PrepareForTest({Banana.class})
- public void testStaticMethod() {
- PowerMockito.mockStatic(Banana.class); //<-- mock静态类
- Mockito.when(Banana.getColor()).thenReturn("绿色");
- Assert.assertEquals("绿色", Banana.getColor());
- //更改类的私有属性
- Whitebox.setInternalState(Banana.class, "COLOR", "红色的");
- }
- }
- mock私有方法
- @RunWith(PowerMockRunner.class)
- public class PowerMockitoPrivateMethodTest {
- @Test
- @PrepareForTest({Banana.class})
- public void testPrivateMethod() throws Exception {
- Banana mBanana = PowerMockito.mock(Banana.class);
- PowerMockito.when(mBanana.getBananaInfo()).thenCallRealMethod();
- PowerMockito.when(mBanana, "flavor").thenReturn("苦苦的");
- Assert.assertEquals("苦苦的黄色的", mBanana.getBananaInfo());
- //验证flavor是否调用了一次
- PowerMockito.verifyPrivate(mBanana).invoke("flavor");
- }
- }
- mock final方法,使用方式同 mock 私有方法
- mock 构造方法
- @Test
- @PrepareForTest({Banana.class})
- public void testNewClass() throws Exception {
- Banana mBanana = PowerMockito.mock(Banana.class);
- PowerMockito.when(mBanana.getBananaInfo()).thenReturn("大香蕉");
- //如果new新对象,则返回这个上面设置的这个对象
- PowerMockito.whenNew(Banana.class).withNoArguments().thenReturn(mBanana);
- //new新的对象
- Banana newBanana = new Banana();
- Assert.assertEquals("大香蕉", newBanana.getBananaInfo());
- }
@Rule用法
自定义@Rule很简单,就是实现TestRule 接口,实现apply方法。
- 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 + "测试结束!");
- }
- };
- }
- }
六、RxJava与单元测试
RxJava的火热程度不用多说,由于其基于事件流的链式调用、逻辑简洁 & 使用简单的特点,深受各大开发者的欢迎。我们经常用它来进行线程的切换操作
例如:
- public void threadSwitch() {
- Observable.just("one", "two", "three", "four", "five")
- .subscribeOn(Schedulers.newThread())
- .observeOn(OpenHarmonySchedulers.mainThread())
- .subscribe(new Observer<String>() {
- @Override
- public void onSubscribe(@NonNull Disposable d) {
- }
- @Override
- public void onNext(@NonNull String s) {
- System.out.println(s);
- if (callBack != null) {
- callBack.success(s);
- }
- }
- @Override
- public void onError(@NonNull Throwable e) {
- if (callBack != null) {
- callBack.failed();
- }
- }
- @Override
- public void onComplete() {
- }
- });
- }
Observable.just执行在子线程中, callBack回调执行在主线程中
基于mockito,我们直接写出对应的单元测试代码:
- @Test
- public void threadSwitch() {
- presenter.threadSwitch();
- // 验证callBack的success方法被调用了5次
- verify(callBack,times(5)).success(anyString());
- }
执行此用例,我们会发现它会报如下错误:
- java.lang.ExceptionInInitializerError
- at io.reactivex.rxjava3.openharmony.schedulers.OpenHarmonySchedulers.lambda$static$0(Unknown Source)
- at io.reactivex.rxjava3.openharmony.plugins.RxOpenHarmonyPlugins.callRequireNonNull(Unknown Source)
- at io.reactivex.rxjava3.openharmony.plugins.RxOpenHarmonyPlugins.initMainThreadScheduler(Unknown Source)
- at io.reactivex.rxjava3.openharmony.schedulers.OpenHarmonySchedulers.<clinit>(Unknown Source)
- at kale.ui.shatter.test.RxSchedulerPresenter.threadSwitch(RxSchedulerPresenter.java:65)
- at kale.ui.shatter.test.RxSchedulerTestTest.threadSwitch(RxSchedulerTestTest.java:52)
- at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
- at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
- at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
- at java.lang.reflect.Method.invoke(Method.java:498)
- at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
- at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
- at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
- at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
- at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
- at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
- at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
- at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
- at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
- at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
- at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
- at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
- at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
- at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
- at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
- at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
- at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
- at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:79)
- at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:85)
- at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:39)
- at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
- at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
- at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
- at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
- at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
- at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
- at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
- at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
- at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
- at java.lang.reflect.Method.invoke(Method.java:498)
- at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:128)
- Caused by: java.lang.RuntimeException: Stub!
- at ohos.eventhandler.EventRunner.getMainEventRunner(EventRunner.java:110)
- at io.reactivex.rxjava3.openharmony.schedulers.OpenHarmonySchedulers$MainHolder.<clinit>(Unknown Source)
- ... 41 more
那么怎么解决呢?那就是设置用到的Schedulers.进行hook,修改用例如下:
- @Test
- public void threadSwitch() {
- RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
- RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
- RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
- RxOpenHarmonyPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
- presenter.threadSwitch();
- // 验证callBack的success方法被调用了5次
- verify(callBack,times(5)).success(anyString());
- }
原理就是当进行线程调度时,都让它切换到Schedulers.trampoline(),这样我们就能正确的输出了。但通常情况下,我们使用到线程切换的场景会很多,这样写毕竟还是不够优雅,稍后我会给出更好的解决方式。
除了上面的线程切换场景,我们还经常会使用到时间轮询之类的场景,例如:
- public void interval() {
- Observable.interval(1, TimeUnit.SECONDS)
- .take(5)
- .flatMap((Function<Long, ObservableSource<String>>)
- aLong -> Observable.just(aLong + ""))
- .subscribeOn(Schedulers.newThread())
- .observeOn(OpenHarmonySchedulers.mainThread())
- .subscribe(new Observer<String>() {
- @Override
- public void onSubscribe(@NonNull Disposable d) {
- }
- @Override
- public void onNext(@NonNull String s) {
- System.out.println(s);
- if (callBack != null) {
- callBack.success(s);
- }
- }
- @Override
- public void onError(@NonNull Throwable e) {
- if (callBack != null) {
- callBack.failed();
- }
- }
- @Override
- public void onComplete() {
- }
- });
- }
我们每隔1秒发射一次数据,一共发送5次,我们写出以下单元测试:
- @Test
- public void interval() {
- RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
- RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
- RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
- RxOpenHarmonyPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
- presenter.interval();
- // 验证callBack的success方法被调用了5次
- verify(callBack,times(5)).success(anyString());
- }
使用上面线程异步变同步的方法确实可以进行测试,但是需要等到5秒后才能执行完成,这显然不符合单元测试执行快的特点。这里,RxJava给我们提供了TestScheduler,调用TestScheduler的advanceTimeTo或advanceTimeBy方法来进行时间操作。
- @Test
- public void interval() {
- TestScheduler testScheduler = new TestScheduler();
- RxJavaPlugins.setIoSchedulerHandler(scheduler -> testScheduler);
- RxJavaPlugins.setComputationSchedulerHandler(scheduler -> testScheduler);
- RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> testScheduler);
- RxOpenHarmonyPlugins.setInitMainThreadSchedulerHandler(scheduler -> testScheduler);
- presenter.interval();
- //将时间设到3秒后
- testScheduler.advanceTimeTo(3,TimeUnit.SECONDS);
- verify(callBack,times(3)).success(anyString());
- //将时间设到10秒后
- testScheduler.advanceTimeTo(10,TimeUnit.SECONDS);
- verify(callBack,times(5)).success(anyString());
- }
这样我们就不用每次执行到该用例的时候,还得等待设定的时间。每次这样写毕竟也不够优雅,下面我给出基于rxjava3和Rxohos:1.0.0,使用TestRule来进行RxJava线程切换及时间操作的工具类,供大家参考:
- /**
- * Created by xiongwg on 2021-07-08.
- * <p>
- * 这个类是让Obserable从异步变同步。
- *
- * 注意: 当有操作时间的测试时,必须调用{@link #setScheduler(Scheduler)}方法
- */
- public class RxJavaTestSchedulerRule implements TestRule {
- /**
- * 运行在当前线程,可异步变同步
- */
- public static final Scheduler DEFAULT_SCHEDULER = Schedulers.trampoline();
- /**
- * 操作时间类的 Scheduler
- */
- public static final Scheduler TIME_SCHEDULER = new TestScheduler();
- private Scheduler mScheduler = DEFAULT_SCHEDULER;
- /**
- * 切换 Scheduler
- *
- * @param scheduler 单元测试用例执行所在的 Scheduler
- */
- public void setScheduler(Scheduler scheduler) {
- if (scheduler != mScheduler) {
- mScheduler = scheduler;
- resetTestSchecduler();
- }
- }
- @Override
- public Statement apply(final Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- resetTestSchecduler();
- base.evaluate();
- }
- };
- }
- public void advanceTimeBy(long delayTime, TimeUnit unit) {
- if (mScheduler instanceof TestScheduler) {
- ((TestScheduler) mScheduler).advanceTimeBy(delayTime, unit);
- }
- }
- public void advanceTimeTo(long delayTime, TimeUnit unit) {
- if (mScheduler instanceof TestScheduler) {
- ((TestScheduler) mScheduler).advanceTimeTo(delayTime, unit);
- }
- }
- public void triggerActions() {
- if (mScheduler instanceof TestScheduler) {
- ((TestScheduler) mScheduler).triggerActions();
- }
- }
- private void resetTestSchecduler() {
- RxJavaPlugins.reset();
- RxJavaPlugins.setIoSchedulerHandler(scheduler -> mScheduler);
- RxJavaPlugins.setComputationSchedulerHandler(scheduler -> mScheduler);
- RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> mScheduler);
- RxOpenHarmonyPlugins.reset();
- RxOpenHarmonyPlugins.setInitMainThreadSchedulerHandler(scheduler -> mScheduler);
- }
使用起来很简单
- // 1、声明RxJavaTestSchedulerRule Rule
- @Rule
- public RxJavaTestSchedulerRule rxJavaTestSchedulerRule = new RxJavaTestSchedulerRule();
- @Test
- public void interval() {
- //2、在需要进行时间操作的方法前,设置Scheduler为TIME_SCHEDULER
- rxJavaTestSchedulerRule.setScheduler(TIME_SCHEDULER);
- presenter.interval();
- //3、操作时间,将时间设置为3秒后
- rxJavaTestSchedulerRule.advanceTimeTo(3, TimeUnit.SECONDS);
- verify(callBack,times(3)).success(anyString());
- //将时间设置为10秒后
- rxJavaTestSchedulerRule.advanceTimeTo(10, TimeUnit.SECONDS);
- verify(callBack,times(5)).success(anyString());
- }
七、其它
Java单元测试中引入了ohos相关类的解决方案
1、尝试Mock出该对象
2、在java单元测试包下新建同包名同类名的Java文件,重写调用到的方法
项目本地查看测试覆盖率
右击需要测试覆盖率的包名 ==> 点击“run test in ‘xxx’ with Coverage”
项目本地查看测试案例通过率