【中软国际】HarmonyOS 非UI单元测试在DevEco Studio上的应用 原创 精华
一、什么是单元测试
单元测试是测试某个类的某个方法能否正常工作的一种手段。
单元测试的粒度:一般一个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”
项目本地查看测试案例通过率
作者:熊文功
很详细!大赞
赞!学习了
赞!学习了
超赞!学习了。
优秀! YYDS,学习了
get到了
赞!!!