一个使用Robolectric完成所有单元测试的项目,企业级脱敏开源项目,供同样搞单测的小伙伴参考。
最近负责搭建所在企业的单元测试框架。发现虽然网上单元测试的项目或者文章众多,但是实际上可以作为商用项目参考的,其实并不多。其原因主要因为如下两点:
1.项目结构偏简单,无法应对企业级项目复杂的场景,比如使用SO等。
2.项目或者文章太老旧,已经失去了参考的意义,网上这一类的文章,最多的往往还是15/16年写的。
所以,想打造一个能够覆盖大多数安卓单元测试场景的项目,来供同样想搞单元测试的小伙伴们来参考,欢迎star/fork/pr。
目前,跑在JVM虚拟机上的单测方案主要是Robolectric,而且这也是google官方推荐的一种单元测试的方案。
单元测试是针对单个方法或类的,自然要排除掉相关的依赖,因此mock工具也是必不可少的。 mock的方案有很多面,主要有以下两种:
mockito:基于cglib的动态代理实现的,可以mock掉替换掉类中的方法,构造方法,静态方法。
powermock:mockito的增强版本,基于JDK动态代理的方式,所以可以实现任何的替换。但是缺点是需要替换原有的运行注解,会导致跑单测有问题,而且很久不维护了。
在实际使用中,发现powermock会对Robolectric的单测运行产生一些影响,并且由于mockito也已经支持了静态方法的mock,所以,最终放弃使用powermock。
采用类对类,方法对方法的规范。
1.如果需要对MainActivity写单元测试,则需要在test文件夹下,相同包名,创建MainActivityTest的单元测试类。
单元测试类的类名为:原类名+Test。
2.方法对方法。针对某个方法写代码单测,推荐使用一对一的场景。
比如针对updateErrorIv方法写单元测试,则单元测试的方法为:testIvErrorState()
当然,涉及到某些具体相关的业务,会出现多个方法对应一个单元测试方法的情况,这种情况也是完全可以的。
gradle版本并不是强制要求,只是方便读者更方便的运行本项目。
gradle-wrapper.properties中配置gradle-6.7.1-all版本。
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
项目中build.gradle配置版本如下:
buildscript {
dependencies {
classpath "com.android.tools.build:gradle:4.2.2"
}
}
app目录下的build.gradle下,配置如下依赖:
testImplementation('junit:junit:4.13.2') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
testImplementation "io.mockk:mockk:1.12.2"
testImplementation "org.assertj:assertj-core:3.22.0"
testImplementation "org.robolectric:robolectric:4.9.2"
testImplementation('org.mockito:mockito-core:3.6.28') {
exclude group: 'net.bytebuddy', module: 'byte-buddy'
exclude group: 'net.bytebuddy', module: 'byte-buddy-agent'
}
testImplementation 'org.mockito:mockito-inline:5.2.0'
testImplementation "androidx.test:core:1.3.0"
testImplementation("org.hamcrest:hamcrest-core:1.3")
testImplementation("org.assertj:assertj-core:2.6.0")
testImplementation 'android.arch.core:core-testing:1.0.0-alpha3'
创建单元测试类,@Before代表执行前的初始化操作。@Test代表执行单元测试操作。
@RunWith(RobolectricTestRunner.class)
public class MainActivityTest {
@Before
public void init() {
APIMock.isUnitTest = true;
}
@Test
public void testAny() {
}
}
相关代码: mvp
页面功能介绍:
1.进入页面,注册自定义监听,退出页面,取消注册自定义监听。
2.首次进入或者回到页面的时候,请求数据并刷新页面,并且只请求一次。
3.展示fragment。
4.点击图标,会弹出dialog。
相关代码: MVPActivity.java
页面代码逻辑梳理:
1.initView方法中,主要为初始化presenter,以及给成员变量中的view赋值;
2.initListener时,初始化listener,并且注册监听。onDestory时,取消注册;
3.init中,请求数据,并且注册fragment;
4.onResume中,非首次进入时要请求数据来刷新页面;
5.refreshPage中,根据所传数据,刷新当前页面;
6.onClick中,点击按钮,展示弹框。
相关代码: MVPPresenter.java
presenter逻辑梳理:
1.onAttach方法进行页面绑定,验证点为:mView不为空。
2.requestInfo方法发起请求,返回值会调用processInfoAndRefreshPage方法进行刷新。
3.processInfoAndRefreshPage方法根据传参不同,执行不同的逻辑判断,然后刷新界面。
因为单元测试基于JVM虚拟机,执行的是java的流程,并不会执行相关的安卓打包流程,所以引用的SO文件不会打包进最终产物当中,因此,直接运行项目,会提示SO找不到。 所以,更合适的方式是把相关的类进行mock替换掉。
如果遇到业务使用到了SO文件,则需要进行对这个对象进行mock,解除依赖。 比如,我们Model层获取数据时,使用到了SO。
public class MVPPresenter{
@Override
public void requestInfo() {
//请求数据,订阅,并显示
Consumer<InfoModel> consumer = this::processInfoAndRefreshPage;
Flowable<InfoModel> observable = DataSource.getInstance().getDataInfo();
Disposable disposable = observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(consumer);
}
}
public class DataSource{
public Flowable<InfoModel> getDataInfo() {
//this us jni
Java2CJNI java2CJNI = new Java2CJNI();
Log.i("SoView", java2CJNI.java2C());
return Flowable.create(emitter -> {
InfoModel infoModel = new InfoModel();
infoModel.status = 100;
infoModel.statusDesc = "fail";
emitter.onNext(infoModel);
}, BackpressureStrategy.BUFFER);
}
}
则我们可以通过mock对象MVPPresenter或DataSource来实现,从而避免请求到真正的getDataInfo()方法。 相关单测代码如下:
MVPPresenter mockPresenter = mock(MVPPresenter.class);
Mockito.doAnswer(invocation -> {
//do nothing
return null;
}).when(mockPresenter).requestInfo();
如果XML引用了某个View的类,并且这个类使用到了SO,则需要整体这个View类。 比如:SoView中使用到了SO文件。 则会产生如下报错,因为
Caused by: java.lang.UnsatisfiedLinkError: no Java2C in java.library.path: [/Users/xxxx/Library/Java/Extensions, /Library/Java/Extensions, /Network/Library/Java/Extensions, /System/Library/Java/Extensions, /usr/lib/java, .]
at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2670)
我们要整体替换掉这个使用SoView的类。
首先,创建SoViewMock类,参考:创建SoViewMock类.java
其次,单元测试类中进行响应的配置,替换掉SoView类。相关代码如下:
@Config(shadows = {SoViewMock.class}, manifest = Config.NONE, sdk = Build.VERSION_CODES.P)
我们Activity对象,一般要使用Robolectric构造的,而不能直接new,否则会不走Activity的生命周期。
但是Activity中的成员变量,则需要使用mock的,因为只有mock的才能进行执行次数以及其它相关的验证,所以,如何替换Activity中的成员变量,就是一个我们要解决的问题。
经过反复的尝试,最终发现了一个可行的方案,即MainActivity的onCreate()中经过handler转发后再使用成员变量进行相关操作,我们在观察到执行完onCreate()方法后,使用mock对象替换原始对象。
//原始类
public class MVPActivity extends Activity{
protected void onCreate(@NonNull Bundle savedInstanceState) {
new Handler().post(() -> {
...使用成员变量进行相关操作
presenter.requestInfo();
};
}
}
//单元测试类
@RunWith(RobolectricTestRunner.class)
public class MVPActivityTest {
@Test
public void testMethod() {
ActivityController<MVPActivity> controller = Robolectric.buildActivity(MVPActivity.class);
MVPPresenter mockPresenter = mock(MVPPresenter.class);
MVPActivity mainActivity = controller.create().start().get();
//替换presenter操作
mainActivity.presenter = mockPresenter;
controller.postCreate(null).resume().visible().topActivityResumed(true);
//避免主线程looper阻塞
ShadowLooper shadowLooper = ShadowLooper.getShadowMainLooper();
shadowLooper.runToEndOfTasks();
//验证操作
verify(mockPresenter, times(1)).requestInfo();
}
}
这样,我们通过使用mock的presenter替换原有Activity中的presenter,从而方便我们对presenter中的相关方法进行验证,并且还不影响Activity的生命周期。
虽然我们可以使用第一种的方案去mock整个单例类,但是这种mock的类,是不方便替换其中的方法。所以,对于单例类,我们可以使用替换getInstance方法的方式来进行替换。
这里以DataAdapaterClient为例,相关类结构如下:
public class DataAdapaterClient {
Map<String, DataChangedListener> listenerMap = new HashMap<>();
public static DataAdapaterClient getInstance() {
return DataAdapaterClient.SingletonHolder.SINGLETON;
}
private static class SingletonHolder {
private static final DataAdapaterClient SINGLETON = new DataAdapaterClient();
private SingletonHolder() {
}
}
}
我们可以通过mock生成DataAdapaterClient对象,然后通过hook掉静态方法getInstance(),实现每次返回的都是我们mock后的ataAdapaterClient对象。相关代码如下:
DataAdapaterClient mockClient = mock(DataAdapaterClient.class);
try (MockedStatic<DataAdapaterClient> ignored2 = mockStatic(DataAdapaterClient.class)) {
when(DataAdapaterClient.getInstance()).thenReturn(mockClient);
}
}
1.使用GPT
2.百度/google
3.参照网上现有的项目,比如:https://github.com/ankidroid/Anki-Android
地址 | 介绍 |
---|---|
https://github.com/ankidroid/Anki-Android | github上单测覆盖率较高的项目 |
https://github.com/mockito/mockito | github上mockito项目 |
https://github.com/robolectric/robolectric | github上robolectric项目 |
https://github.com/powermock/powermock | github上powermock项目 |
https://developer.android.com/reference/androidx/test/core/app/ActivityScenario | 官方关于ActivityScenario的介绍ActivityScenario用于替代ActivityController |