Robolectric 實(shí)戰(zhàn)解耦整個系列:
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(View 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(Presenter 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試本地json測試
github的鏈接: https://github.com/drchengit/Robolectric_arm_mvp
我的項(xiàng)目用的是jassyan的arm快速開發(fā)框架!
我做robolectric單元測試時,想哭1咎椤样漆!
因?yàn)樗膁agger2 用得飛起放祟,我寫代碼的時候一邊用一邊說著:
"牛啤了!大兄弟眉撵!"
“哦纽疟!還能這么用,牛企∷痢!牛啤”
但是在解耦單元測試的時候:
“這是啥喲亡问!這個東西從哪里來的?”
“又報一個空指針”
“我是照著demo代碼一步步敲下來的酝陈!報錯是啥子情況锈死?”
如果你用了arm 而且要做Robolectric 單元測試的話其屏,不妨看看喲偎行!如果不會Robolectric 建議先學(xué)學(xué)再看:測試資源放送
第一步: 寫個登錄功能
LoginActvity
public class LoginActivity extends BaseActivity<LoginPresenter> implements LoginContract.View {
···
@OnClick(R.id.tv_login)
public void onViewClicked() {
mPresenter.login();
}
···
}
LoginPresenter
public class LoginPresenter extends BasePresenter<LoginContract.Model, LoginContract.View> {
···
public void login() {
if(mRootView.getMobileStr().length() != 11){
mRootView.showMessage("手機(jī)號碼不正確");
return;
}
if(mRootView.getPassWordStr().length() < 1){
mRootView.showMessage("密碼太短");
return;
}
//調(diào)用登錄接口妙真,正確的密碼:abc 手機(jī)號只要等于11位判斷賬號為正確
mModel.login(mRootView.getMobileStr(),mRootView.getPassWordStr())
.compose(RxUtils.applySchedulers(mRootView))
.subscribe(new MyErrorHandleSubscriber<User>(mErrorHandler) {
//這個類是我自定義的一個類,統(tǒng)一攔截所有error 并回調(diào)給: ResponseErrorListenerImpl
// 可以不統(tǒng)一處理菱阵,直接重寫覆蓋:
// @Override
// public void onError(@NonNull Throwable t) {}
@Override
public void onNext(User user) {
mRootView.loginSuccess();
}
});
}
···
}
LoginModel
public class LoginModel extends BaseModel implements LoginContract.Model {
···
@Override
public Observable<User> login(String mobileStr, String passWordStr) {
//調(diào)用登錄接口嗤堰,正確的密碼:abc 手機(jī)號只要等于11位判斷賬號為正確
String name;
if(passWordStr.equals("abc")){//正確密碼琳钉,
name = "drchengit";
}else {
name = "drchengi";
}
//由于不知道上哪里去找一個穩(wěn)定且長期可用的登錄接口,所以用的接口是github 上的查詢接口:https://api.github.com/users/drchengit
// 這里的處理是正確的密碼,請求存在的用戶名:drchengit 錯誤的密碼請求不存在的用戶名: drchengi
// 將就一下
return mRepositoryManager.obtainRetrofitService(CommonService.class).getUser(name);
}
···
}
注意我通過Okhttp 的插值器 回調(diào)GlobalHttpHandlerImpl 類的onHttpResultResponse()方法,如果返回 "no found" 內(nèi)部會 throw 一個 "密碼錯誤" 的自定義異常慨蓝,被框架捕獲并打印 Toast
public class GlobalHttpHandlerImpl implements GlobalHttpHandler {
@Override
public Response onHttpResultResponse(String httpResult, Interceptor.Chain chain, Response response) {
if (!TextUtils.isEmpty(httpResult) && RequestInterceptor.isJson(response.body().contentType())) {
User user;
// https://blog.csdn.net/qfikh/article/details/75669939
// List<User> list = ArmsUtils.obtainAppComponentFromContext(context).gson().fromJson(httpResult, new TypeToken<List<User>>() {
// }.getType());
user = ArmsUtils.obtainAppComponentFromContext(context).gson().fromJson(httpResult, User.class);
if(user.isLoginFaild()){
throw new MyNetException(10001,"密碼錯誤");
}
}
return response;
}
···
}
我省略了過程,總之就是輸入正確的手機(jī)號和密碼就可以登錄,輸錯就會提示"密碼錯誤"。
第二步導(dǎo)包和配置
其實(shí)androidx 已經(jīng)出了,https://github.com/robolectric/robolectric,但是jessyan在簡書回復(fù)我androidx 現(xiàn)在沒打算適配(第一次收到作者的回復(fù),可把我牛逼壞了,學(xué)android 的人都這么平易近人嗎?)
我也還有沒有處理遷移的bug,所以用了這框架只有將就sdk 27版本的測試用一下。
android {
//單元測試
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
·····
//單元測試
testImplementation 'org.robolectric:robolectric:3.8'
testImplementation "org.robolectric:shadows-support-v4:3.4-rc2"
//依賴隔離
testImplementation "org.mockito:mockito-core:2.11.0"
}
}
注意: includeAndroidResources = true這要加上
第三步測試View 層
-
新建
ctrl + shift + T
- 寫好最基本測試迫不急待地運(yùn)行
package me.jessyan.mvparms.demo.mvp.ui.activity.login;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowToast;
import static org.junit.Assert.*;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 27)
public class LoginActivityTest {
TextView loginTv;
EditText phoneEt;
EditText passWrodEt;
private LoginActivity loginActivity;
@Before
public void setUp() {
ShadowLog.stream = System.out;
loginActivity = Robolectric.buildActivity(LoginActivity.class)
.create()
.resume()
.get();
loginTv = loginActivity.findViewById(R.id.tv_login);
phoneEt = loginActivity.findViewById(R.id.et_mobile);
passWrodEt = loginActivity.findViewById(R.id.et_pass);
}
@Test
public void login(){
//直接點(diǎn)擊登錄
loginTv.performClick();
Assert.assertEquals("手機(jī)號碼不正確", ShadowToast.getTextOfLatestToast());
}
}
-
第一個問題,泄露框架出問題,點(diǎn)過去看下,AppLifecyclesImpl類空針針
public class AppLifecyclesImpl implements AppLifecycles {
···
@Override
public void onCreate(@NonNull Application application) {
try {
if (LeakCanary.isInAnalyzerProcess(application)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
}catch (NullPointerException e){
}
···
}
···
}
LeakCanary 是內(nèi)存泄露捏浊,對單元測試沒啥用,直接try {}catch
- 再次運(yùn)行胡岔,ok靶瘸,一路綠燈,下面進(jìn)行登錄接口測試
@Test
public void login(){
//直接點(diǎn)擊登錄
// loginTv.performClick();
// Assert.assertEquals("手機(jī)號碼不正確", ShadowToast.getTextOfLatestToast());
phoneEt.setText("13547250999");
//沒有輸入密碼
// loginTv.performClick();
// Assert.assertEquals("密碼太短", ShadowToast.getTextOfLatestToast());
//錯誤密碼
passWrodEt.setText("aaaa");
loginTv.performClick();
//這里是驗(yàn)證網(wǎng)絡(luò)框架提示
Assert.assertEquals("密碼錯誤", ShadowToast.getTextOfLatestToast());
}
-
報了null,根本沒有打toast
-
debug了半天,發(fā)現(xiàn)沒有回調(diào)
- 查了一下,原來測試要線程同步帜平,于是加上同步代碼冗锁。
(下面的代碼讓Rxjava io線程和android 的main 線程同步)
private void initRxJava() {
RxJavaPlugins.reset();
RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
RxAndroidPlugins.reset();
RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
}
-
心想現(xiàn)在應(yīng)該沒有問題了吧!結(jié)果還是同樣的問題,沒有打印toast摔敛!
接下來我就進(jìn)入了終極debug和翻源碼模式,終于看到了這樣一段代碼
@Singleton
public class RepositoryManager implements IRepositoryManager {
···
private <T> T createWrapperService(Class<T> serviceClass) {
// 通過二次代理,對 Retrofit 代理方法的調(diào)用包進(jìn)新的 Observable 里在 io 線程執(zhí)行。
return (T) Proxy.newProxyInstance(serviceClass.getClassLoader(),
new Class<?>[]{serviceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
if (method.getReturnType() == Observable.class) {
// 如果方法返回值是 Observable 的話,則包一層再返回
return Observable.defer(() -> {
final T service = getRetrofitService(serviceClass);
// 執(zhí)行真正的 Retrofit 動態(tài)代理的方法
}
// 返回值不是 Observable 的話不處理
final T service = getRetrofitService(serviceClass);
return getRetrofitMethod(service, method).invoke(service, args);
}
});
}
···
}
- 莫不是sign線程沒有同步乎莉?惋啃?绒瘦?加上Sign()線程同步予跌,調(diào)用initRxjava方法
private void initRxJava() {
RxJavaPlugins.reset();
RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
//這個喲
RxJavaPlugins.setSingleSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
RxAndroidPlugins.reset();
RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
}
- ok,綠燈,加上完整測試登錄邏輯
@Test
public void login(){
initRxJava();
//直接點(diǎn)擊登錄
loginTv.performClick();
Assert.assertEquals("手機(jī)號碼不正確", ShadowToast.getTextOfLatestToast());
phoneEt.setText("13547250999");
//沒有輸入密碼
loginTv.performClick();
Assert.assertEquals("密碼太短", ShadowToast.getTextOfLatestToast());
//錯誤密碼
passWrodEt.setText("aaaa");
loginTv.performClick();
//這里是驗(yàn)證網(wǎng)絡(luò)框架提示
Assert.assertEquals("密碼錯誤", ShadowToast.getTextOfLatestToast());
//正確密碼登錄
passWrodEt.setText("abc");
loginTv.performClick();
Assert.assertEquals("登錄成功",ShadowToast.getTextOfLatestToast());
//驗(yàn)證跳轉(zhuǎn)
ShadowActivity shadowActivity = Shadows.shadowOf(loginActivity);
Intent intent = shadowActivity.getNextStartedActivity();
Assert.assertEquals(intent.getComponent().getClassName(), MainActivity.class);
}
- 一路綠燈,到這里View 層的Robolectric單元測試 才算完成膳殷,后面是Presenter 的業(yè)務(wù)解耦
Robolectric 實(shí)戰(zhàn)解耦整個系列:
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(View 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(Presenter 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試本地json測試
github的鏈接: https://github.com/drchengit/Robolectric_arm_mvp
測試資源放送
基本的配置 | http://www.reibang.com/p/7a4024925193
常見的坑(分包導(dǎo)致測試報錯等) | https://blog.csdn.net/weixin_34204057/article/details/91418305
我是drchen勒极,一個溫潤的男子,版權(quán)所有匾七,未經(jīng)允許不得抄襲。