Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Andriod 单元测试(四)—— Robolectric&Mockito #180

Open
soapgu opened this issue Dec 7, 2022 · 0 comments
Open

Andriod 单元测试(四)—— Robolectric&Mockito #180

soapgu opened this issue Dec 7, 2022 · 0 comments
Labels
Demo Demo 安卓 安卓

Comments

@soapgu
Copy link
Owner

soapgu commented Dec 7, 2022

  • 将单元测试进行到底

    这次我们的研究范围是在JVM内可运行为止。因为如果牵涉模拟设备或者外接设备,测试的效率和复杂度都会增加一个等级。所以现阶段的Research还是在JVM范围内,这样就可以很好的和CI/CD绑定,也为以后TDD打下基础,虽然现在觉得喊TDD和共产主义差不多~~~
    前面的单元测试对于安卓本身的组件测试还是太薄弱了,但是又不想动用到安卓系统怎么办?
    Robolectric是一个框架,正好满足了我们需求,完成了既要又要到要求

  • 快速入门

首先增加依赖
testImplementation 'org.robolectric:robolectric:4.8'
这个依赖是只有测试才会用到,打包apk不会被打进去
另外

android {
  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
}

这个配置一定要加,默认测试是不会加载resource的。我一开始没加,Activity直接创建异常,提示资源找不到

写个测试类,要@RunWith(RobolectricTestRunner.class)

package com.demo.myunittest;

import android.widget.TextView;

import static org.junit.Assert.*;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;

@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {

    @Test
    public void helloWorld(){
        try (ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class)) {
            controller.setup();
            MainActivity activity = controller.get();
            TextView textView = activity.findViewById(R.id.tv_message);
            assertEquals( "Hello World!",textView.getText() );
        }
    }
}

图片

测试通过
  • 好像还缺了一点啥?

虽然可以直接调用安卓的的对象,但是不是所有的上下文可以直接获取,有些还是需要Mock。怎么处理那?
在robolectric官方wiki里面建议用Using PowerMock
不过PowerMock似乎“太重”,我还是先试试轻一点的Mockito

  • Mockito快速入门

增加依赖testImplementation 'org.mockito:mockito-core:4.9.0'

写一个简单待测试类和测试类

package com.demo.myunittest.util;

public class TestMock {
    public String echo(){
        return "Hello mock from class";
    }
}


public class MockTest {
    @Test
    public void helloWorld( ){
        TestMock mockObject = mock(TestMock.class);
        when(mockObject.echo()).thenReturn("changed mock value");
        assertEquals( "changed mock value",mockObject.echo() );
    }
}

调用when() thenReturn()完成数据打桩
测试通过~

那如果是静态方法怎么办?

public class TestMock {
    public String echo(){
        return "Hello mock from class";
    }

    public static String staticOutput(){
        return "static output";
    }
}

 @Test
    public void helloStatic() {
        try (MockedStatic mocked = mockStatic(TestMock.class)) {
            mocked.when(TestMock::staticOutput).thenReturn("changed static mock value");
            assertEquals( "changed static mock value", TestMock.staticOutput() );
        }
    }

参考static_mocks文档

结果出错了

The used MockMaker SubclassByteBuddyMockMaker does not support the creation of static mocks

Mockito's inline mock maker supports static mocks based on the Instrumentation API.
You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'.
Note that Mockito's inline mock maker is not supported on Android.
org.mockito.exceptions.base.MockitoException: 
The used MockMaker SubclassByteBuddyMockMaker does not support the creation of static mocks

看来还是要换依赖
文档也说得很清楚了,是自己没耐心看When using the inline mock maker, it is possible to mock static method invocations within the current thread and a user-defined scope

要换成 testImplementation 'org.mockito:mockito-inline:4.9.0'
测试通过。

  • 再进一步需要静态方法和成员方法混合用怎么办?就比如HttpClientWrapper.getInstance().ResponseJson
    有办法,俄罗斯套娃
   @Test
    public void testResponseJson() {
        try( MockedStatic<HttpClientWrapper> mocked = mockStatic(HttpClientWrapper.class) ){
            HttpClientWrapper instance = mock(HttpClientWrapper.class);
            UuidResponse response = new UuidResponse();
            UUID uuid = UUID.randomUUID();
            response.setUuid(uuid.toString());
            when(instance.ResponseJson(any(), any()))
                    .thenReturn(Single.just(response));
            mocked.when( HttpClientWrapper::getInstance )
                    .thenReturn(instance);
            String url = "https://httpbin.org/uuid";
            Request request = new Request.Builder()
                    .url(url)
                    .get()
                    .build();
            TestObserver<UuidResponse> testObserver = HttpClientWrapper.getInstance().ResponseJson(request, UuidResponse.class)
                    .test();
            testObserver.awaitCount(1);
            testObserver.assertComplete();
            assertEquals(uuid.toString(), testObserver.values().get(0).getUuid());
        }
    }
  • Let's together!

好基础全打好了,把两部分代码合起来测试看看!

报错了!看来不能一帆风顺!
看上去虽然点击了按钮,但是值没有发生改变

org.junit.ComparisonFailure: expected:<[d5904b0e-bc12-46ec-91ec-e9b246c144f4]> but was:<[Hello World!]>
	at org.junit.Assert.assertEquals(Assert.java:117)
	at org.junit.Assert.assertEquals(Assert.java:146)
	at com.demo.myunittest.RobolectricTest.testClickAndMockResponse(RobolectricTest.java:54)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	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.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:591)
	at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:274)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:88)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)
	Suppressed: org.robolectric.android.internal.AndroidTestEnvironment$UnExecutedRunnablesException: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(Looper.getMainLooper()).idle() call.

根据提示在点击按钮操作和assert之间增加shadowOf(Looper.getMainLooper()).idle()

又又失败了,这次报另一个错。

不能访问BackdropFrameRenderer
找不到com.android.internal.policy.BackdropFrameRenderer的类文件

用谷歌搜索搞定,定位在robolectric的github的issues里面

解决方案就是升级到4.8.1就好!好粗暴!

最终合成代码

    @Test
    public void testClickAndMockResponse(){
        try (ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class)) {
            controller.setup();
            try( MockedStatic<HttpClientWrapper> mocked = mockStatic(HttpClientWrapper.class) ) {
                HttpClientWrapper instance = mock(HttpClientWrapper.class);
                UuidResponse response = new UuidResponse();
                UUID uuid = UUID.randomUUID();
                response.setUuid(uuid.toString());
                when(instance.ResponseJson(any(), any()))
                        .thenReturn(Single.just(response));
                mocked.when(HttpClientWrapper::getInstance)
                        .thenReturn(instance);
                MainActivity activity = controller.get();
                TextView textView = activity.findViewById(R.id.tv_message);
                activity.findViewById(R.id.button_test).performClick();
                shadowOf(Looper.getMainLooper()).idle();
                assertEquals( uuid.toString(),textView.getText() );
            }
        }
    }

执行时间大概3秒,比普通Test Case要长一点点

  • 任重道远的单元测试

对于安卓的类,虽然Robolectric可以帮我们模拟一些,但是在一些具体执行上还需要更多辅助。
比如,前面我点击按钮后,其实是Rx订阅后是把更新UI的操作“推给”主线程
但是单元测试里面的方法performClick并不能“马上”更新UI。

Mockito目前只能针对一些我们自定义的类,对于安卓复杂类不太合适。

Caution: Complex mocks should be avoided. Instead, you can use different types of test doubles such as fakes, or Robolectric shadows if they are Android classes. Also, consider using the real implementation of the dependency in an instrumentation test.

另外Unit Test是一个拆解的过程。当初开发是好不容易都合成到一起了,现在重新分解。目前还是针对最简单的demo。
下一篇可以往IoC方向的单元测试研究下

@soapgu soapgu changed the title Andriod 单元测试(四)—— Robolectric Andriod 单元测试(四)—— Robolectric&mockito Dec 8, 2022
@soapgu soapgu changed the title Andriod 单元测试(四)—— Robolectric&mockito Andriod 单元测试(四)—— Robolectric&Mockito Dec 8, 2022
@soapgu soapgu added 安卓 安卓 Demo Demo labels Dec 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Demo Demo 安卓 安卓
Projects
None yet
Development

No branches or pull requests

1 participant