本篇介绍如何对MVP架构的项目进行单元测试。会用到之前六篇文章中所介绍的内容,算是学以致用了。本文中我没具体说明的地方前几篇文中一定会有的。希望大家可以循序渐进。
对于MVP网上也有很多变种,各有千秋,但是万变不离其宗。本篇采用的MVP是《Android源码设计模式解析与实战》这本书中介绍的一种,我也有写过相关的读书笔记,没看过这本书的可以简单了解下。我本人还是很喜欢这种MVP的。
1.MVP相关基类
View的接口 : MvpView
public interface MvpView {
/*** * 获取Context * @return Context */
Context getContext();
/*** * 显示Progress */
void showProgress();
/*** * 关闭Progress */
void closeProgress();
/*** * @param string 消息内容 */
void showToast(String string);
}
扮演着view和model的中间层的角色 : BaseMVPPresenter
public abstract class BaseMVPPresenter<T extends MvpView> {
/** * View接口类型的弱引用 */
private Reference<T> mViewRef;
protected T mMvpView;
/** * 建立关联 */
public void attachView(T view){
mViewRef = new WeakReference<>(view);
if(isViewAttached()) {
mMvpView = getView();
}
}
/** * 获取View * @return View */
public T getView(){
return mViewRef.get();
}
/** * UI展示相关的操作需要判断一下 Activity 是否已经 finish. * <p> * todo : 只有当 isActivityAlive 返回true时才可以执行与Activity相关的操作, * 比如 弹出Dialog、Window、跳转Activity等操作. * * @return boolean */
public boolean isViewAttached(){
return mViewRef != null && mViewRef.get() != null;
}
/** * 解除关联 */
public void detachView(){
if( mViewRef != null){
mViewRef.clear();
mViewRef = null;
}
}
}
简单封装的view : BaseMVPActivity
public abstract class BaseMVPActivity<V extends MvpView, T extends BaseMVPPresenter<V>> extends AppCompatActivity implements MvpView{
/** * Presenter对象 */
protected T mPresenter;
public ProgressDialog mProgress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPresenter = createPresenter();
mPresenter.attachView((V)this);
mProgress = new ProgressDialog(this);
mProgress.setMessage("加载中...");
}
@Override
protected void onDestroy() {
if (mPresenter != null){
mPresenter.detachView();
}
super.onDestroy();
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (mPresenter == null){
mPresenter = createPresenter();
}
}
/** * 创建Presenter对象 * @return Presenter对象 */
protected abstract T createPresenter();
@Override
public Context getContext() {
return this;
}
@Override
public void showProgress() {
if (mProgress != null && !mProgress.isShowing()){
mProgress.show();
}
}
@Override
public void closeProgress() {
if (mProgress != null && mProgress.isShowing()) {
mProgress.dismiss();
}
}
@Override
public void showToast(String string) {
Toast.makeText(this, string, Toast.LENGTH_SHORT).show();
}
}
2.举栗子
这次我们还是采用上一篇的例子。一个简单的登录页面,其中有两个功能:
-
获取验证码(点击获取验证码后,实现一个120s的倒计时)
-
登录(验证输入的手机号码与验证码,请求登录接口)
代码很简单,我一一的贴出来:
public interface LoginMvpView extends MvpView{
/** * 倒计时完成 */
void countdownComplete();
/** * 倒计时中 * @param time 剩余时间 */
void countdownNext(String time);
/** * 登录成功 */
void loginSuccess();
}
public class LoginPresenter extends BaseMVPPresenter<LoginMvpView>{
private CompositeDisposable mCompositeDisposable = new CompositeDisposable();
public void getIdentify() {
// interval隔一秒发一次,到120结束
Disposable mDisposable = Observable
.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
.take(120)
.subscribeWith(new DisposableObserver<Long>() {
@Override
public void onComplete() {
mMvpView.countdownComplete();
}
@Override
public void onError(Throwable e) {
mMvpView.showToast("倒计时出现错误!");
}
@Override
public void onNext(Long aLong) {
mMvpView.countdownNext(String.valueOf(Math.abs(aLong - 120)));
}
});
mCompositeDisposable.add(mDisposable);
}
public void login(String mobile, String code) {
if(mobile.length() != 11){
mMvpView.showToast("手机号码不正确");
return;
}
if(code.length() != 6){
mMvpView.showToast("验证码不正确");
return;
}
GithubService.createGithubService()
.getUser("simplezhli")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(new Consumer<Disposable>() {
@Override
public void accept(Disposable disposable) throws Exception {
if (isViewAttached()){
mMvpView.showProgress();
}
}
})
.doAfterTerminate(new Action() {
@Override
public void run() throws Exception {
if (isViewAttached()){
mMvpView.closeProgress();
}
}
})
.subscribe(new Observer<User>() {
@Override
public void onSubscribe(Disposable d) {
mCompositeDisposable.add(d);
}
@Override
public void onNext(User user) {
mMvpView.showToast("登录成功");
mMvpView.loginSuccess();
}
@Override
public void onError(Throwable e) {
mMvpView.showToast("登录失败");
}
@Override
public void onComplete() {}
});
}
@Override
public void detachView(){
super.detachView();
mCompositeDisposable.clear();
}
}
public class LoginMVPActivity extends BaseMVPActivity<LoginMvpView, LoginPresenter> implements LoginMvpView, View.OnClickListener{
private TextView mTvSendIdentify;
private EditText mEtMobile;
private EditText mEtIdentify;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
mEtMobile = (EditText) this.findViewById(R.id.et_mobile);
mEtIdentify = (EditText) this.findViewById(R.id.et_identify);
mTvSendIdentify = (TextView) this.findViewById(R.id.tv_send_identify);
this.findViewById(R.id.tv_login).setOnClickListener(this);
mTvSendIdentify.setOnClickListener(this);
}
@Override
protected LoginPresenter createPresenter() {
return new LoginPresenter();
}
@Override
public void countdownComplete() {
mTvSendIdentify.setText(R.string.login_send_identify);
mTvSendIdentify.setEnabled(true);
}
@Override
public void countdownNext(String time) {
mTvSendIdentify.setText(TextUtils.concat(time, "秒后重试"));
}
@Override
public void loginSuccess() {
showToast("登录成功");
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.tv_send_identify:
mTvSendIdentify.setEnabled(false);
mPresenter.getIdentify();
break;
case R.id.tv_login:
mPresenter.login(mEtMobile.getText().toString().trim(),
mEtIdentify.getText().toString().trim());
break;
default:
break;
}
}
}
实现代码很简单,我就不具体说明了,主要说说单元测试部分。
3.单元测试
单元测试主要测试两部分:Activity
与Presenter
。
-
Activity
部分其实和上一篇大同小异,主要是测试界面上的View
的状态变化和文字显示,Toast
,Dialog
的弹出与显示内容这些是否符合预期。 -
Presenter
部分测试数据处理的正确性,回调接口的次数与内容是否符合预期。
Activity
测试部分代码:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class LoginMVPActivityTest {
private LoginMVPActivity loginActivity;
private TextView mTvSendIdentify;
private TextView mTvLogin;
private EditText mEtMobile;
private EditText mEtIdentify;
@Rule
public RxJavaTestSchedulerRule rule = new RxJavaTestSchedulerRule();
@Before
public void setUp(){
ShadowLog.stream = System.out;
loginActivity = Robolectric.setupActivity(LoginMVPActivity.class);
mTvSendIdentify = (TextView) loginActivity.findViewById(R.id.tv_send_identify);
mTvLogin = (TextView) loginActivity.findViewById(R.id.tv_login);
mEtMobile = (EditText) loginActivity.findViewById(R.id.et_mobile);
mEtIdentify = (EditText) loginActivity.findViewById(R.id.et_identify);
}
@Test
public void testGetIdentify() throws Exception {
Application application = RuntimeEnvironment.application;
assertEquals(mTvSendIdentify.getText().toString(),
application.getString(R.string.login_send_identify));
// 触发按钮点击
mTvSendIdentify.performClick();
// 时间到10秒
rule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);
assertEquals(mTvSendIdentify.isEnabled(), false);
assertEquals(mTvSendIdentify.getText().toString(), "111秒后重试");
// 时间到120秒
rule.getTestScheduler().advanceTimeTo(120, TimeUnit.SECONDS);
assertEquals(mTvSendIdentify.getText().toString(),
application.getString(R.string.login_send_identify));
assertEquals(mTvSendIdentify.isEnabled(), true);
}
@Test
public void testLogin() throws Exception {
mEtMobile.setText("123");
mEtIdentify.setText("123");
mTvLogin.performClick();
assertEquals("手机号码不正确", ShadowToast.getTextOfLatestToast());
mEtMobile.setText("13000000000");
mEtIdentify.setText("123");
mTvLogin.performClick();
assertEquals("验证码不正确", ShadowToast.getTextOfLatestToast());
initRxJava();
mEtMobile.setText("13000000000");
mEtIdentify.setText("123456");
mTvLogin.performClick();
// 判断ProgressDialog弹出
assertNotNull(ShadowProgressDialog.getLatestDialog());
assertEquals("登录成功", ShadowToast.getTextOfLatestToast());
}
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();
}
});
}
}
Presenter
测试部分代码:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class LoginPresenterTest{
private LoginPresenter mPresenter;
@Mock
private LoginMvpView mvpView;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Rule
public RxJavaTestSchedulerRule rule = new RxJavaTestSchedulerRule();
@Before
public void setUp(){
//输出日志
ShadowLog.stream = System.out;
mPresenter = new LoginPresenter();
mPresenter.attachView(mvpView);
}
@Test
public void testGetIdentify() throws Exception {
mPresenter.getIdentify();
// 时间到10秒
rule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);
// 验证方法被调用10次
verify(mvpView, times(10)).countdownNext(anyString());
// 时间到120秒
rule.getTestScheduler().advanceTimeTo(120, TimeUnit.SECONDS);
verify(mvpView, times(120)).countdownNext(anyString());
// 验证倒计时完成方法被调用
verify(mvpView).countdownComplete();
}
@Test
public void testLogin() throws Exception {
initRxJava();
mPresenter.login("123", "123");
verify(mvpView).showToast("手机号码不正确");
mPresenter.login("13000000000", "123");
verify(mvpView).showToast("验证码不正确");
mPresenter.login("13000000000", "123456");
verify(mvpView).showProgress();
verify(mvpView).loginSuccess();
verify(mvpView).closeProgress();
}
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();
}
});
}
}
本篇内容不多,主要是前面讲解内容的一个整合,下一篇会说说MVP结合Dagger的单元测试。所有代码已上传至Github。希望大家多多点赞支持!
今天的文章Android单元测试(七):MVP与单元测试分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22874.html