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
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”

項目本地查看測試案例通過率


