HarmonyOS非UI单元测试在DevEco Studio上的应用
想了解更多内容,单元请访问:
和华为官方合作共建的测试鸿蒙技术社区
https://harmonyos.51cto.com
一、什么是应用单元测试
单元测试是测试某个类的某个方法能否正常工作的一种手段。
单元测试的单元粒度:一般一个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四、b2b供应网单元测试流程
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”

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



想了解更多内容,请访问:
和华为官方合作共建的鸿蒙技术社区
https://harmonyos.51cto.com