【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用 原创 精华

深开鸿开发板
发布于 2021-7-27 17:32
浏览
8收藏

一、什么是单元测试

单元测试是测试某个类的某个方法能否正常工作的一种手段。
单元测试的粒度:一般一个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<T> 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”
【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用-鸿蒙开发者社区
【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用-鸿蒙开发者社区
【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用-鸿蒙开发者社区

项目本地查看测试案例通过率

【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用-鸿蒙开发者社区
【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用-鸿蒙开发者社区
【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用-鸿蒙开发者社区

作者:熊文功

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2021-7-28 14:31:39修改
15
收藏 8
回复
举报
7条回复
按时间正序
/
按时间倒序
Der_带鱼
Der_带鱼

很详细!大赞

1
回复
2021-7-27 19:07:51
wx60114fc31e6c1
wx60114fc31e6c1

赞!学习了

回复
2021-7-28 14:24:11
张花花
张花花

赞!学习了

回复
2021-7-29 17:51:26
mb607a438e1e09f
mb607a438e1e09f

超赞!学习了。

回复
2021-8-2 10:01:27
AnBetter2021
AnBetter2021

优秀! YYDS,学习了

回复
2021-8-4 09:18:50
飞中缘a
飞中缘a

get到了

 

回复
2021-8-5 14:04:44
爆炸小清新
爆炸小清新

赞!!!

回复
2021-8-11 14:28:54
回复
    相关推荐