Android单元测试片段在MPV中的应用
我正在使用经典的MVP方法重新发明我的应用程序。为此,我阅读了许多文章和教程,我得出的结论是最好的方法是:Android单元测试片段在MPV中的应用,android,unit-testing,mvp,robolectric,android-testing,Android,Unit Testing,Mvp,Robolectric,Android Testing,我正在使用经典的MVP方法重新发明我的应用程序。为此,我阅读了许多文章和教程,我得出的结论是最好的方法是: 为演示者和视图创建一个界面 使片段和活动实现视图接口 创建presenter接口的实现,该接口在构造函数中接受它管理的视图的实例,并在视图的实现中保存对presenter的引用 所以我创建了这个类 视图界面 public interface SignupEmailView extends BaseView { void fillEmail(String email)
- 为演示者和视图创建一个界面
- 使片段和活动实现视图接口
- 创建presenter接口的实现,该接口在构造函数中接受它管理的视图的实例,并在视图的实现中保存对presenter的引用
public interface SignupEmailView extends BaseView {
void fillEmail(String email);
void onEmailInvalid(String error);
void onDataValidated();
}
public interface SignupEmailPresenter {
void initData(Bundle bundle);
void validateData(String email);
}
演示者界面
public interface SignupEmailView extends BaseView {
void fillEmail(String email);
void onEmailInvalid(String error);
void onDataValidated();
}
public interface SignupEmailPresenter {
void initData(Bundle bundle);
void validateData(String email);
}
视图实现
public class FrSignup_email extends BaseSignupFragmentMVP implements IBackHandler, SignupEmailView {
public static String PARAM_EMAIL = "param_email";
@Bind(R.id.signup_step2_new_scrollview)
ScrollView mScrollview;
@Bind(R.id.signup_step2_new_lblTitle)
SuperLabel mLblTitle;
@Bind(R.id.signup_step2_new_lblSubtitle)
TextView mLblSubtitle;
@Bind(R.id.signup_step2_new_txtEmail)
EditText mTxtEmail;
@Bind(R.id.signup_step2_new_btnNext)
Button mBtnNext;
protected SignupActivityView mActivity;
SignupEmailPresenter mPresenter;
public FrSignup_email() {
// Required empty public constructor
}
public static FrSignup_email newInstance(String email) {
FrSignup_email fragment = new FrSignup_email();
Bundle b = new Bundle();
b.putString(PARAM_EMAIL, email);
fragment.setArguments(b);
return fragment;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mActivity = (SignupActivityView) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement IResetPasswordBridge");
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = loadView(inflater, container, savedInstanceState, R.layout.fragment_signup_email);
mPresenter = new SignupEmailPresenterImpl(this);
ButterKnife.bind(this, view);
return view;
}
@Override
public final void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
applyCircularReveal();
mPresenter.initData(this.getArguments());
mTxtEmail.setImeOptions(EditorInfo.IME_ACTION_NEXT);
mTxtEmail.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
mPresenter.validateData(mTxtEmail.getText().toString());
return true;
}
return false;
}
});
mTxtEmail.setOnTouchListener(new OnTouchCompoundDrawableListener_NEW(mTxtEmail, new OnTouchCompoundDrawableListener_NEW.OnTouchCompoundDrawable() {
@Override
public void onTouch() {
mTxtEmail.setText("");
}
}));
mBtnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.validateData(mTxtEmail.getText().toString());
}
});
}
@Override
public void fillEmail(String email) {
mTxtEmail.setText(email);
}
@Override
public void onEmailInvalid(String error) {
displayError(error);
}
@Override
public void onDataValidated() {
changeFieldToValid(mTxtEmail);
setEmail(mTxtEmail.getText().toString());
// the activity shows the next fragment
mActivity.onEmailValidated();
}
@Override
public boolean doBack() {
if (!isLoading()) {
mActivity.onEmailBack();
}
return true;
}
@Override
public void displayError(String error) {
changeFieldToInvalid(mTxtEmail);
mLblSubtitle.setText(error);
mLblSubtitle.setTextColor(ContextCompat.getColor(getActivity(), R.color.field_error));
}
}
演示者实现
public class SignupEmailPresenterImpl implements SignupEmailPresenter {
private SignupEmailView mView;
public SignupEmailPresenterImpl(SignupEmailView view) {
mView = view;
}
@Override
public void initData(Bundle bundle) {
if (bundle != null) {
mView.fillEmail(bundle.getString(FrSignup_email.PARAM_EMAIL));
}
}
@Override
public void validateData(String password) {
ValidationUtils_NEW.EmailStatus status = ValidationUtils_NEW.validateEmail(password);
if (status != ValidationUtils_NEW.EmailStatus.VALID) {
mView.onEmailInvalid(ValidationUtils_NEW.getEmailErrorMessage(status));
} else {
mView.onDataValidated();
}
}
}
现在,片段由一个实现此视图接口的活动持有,该活动有自己的演示者
public interface SignupActivityView extends BaseView {
void onEmailValidated();
void onPhoneNumberValidated();
void onPasswordValidated();
void onUnlockCodeValidated();
void onResendCodeClick();
void onEmailBack();
void onPhoneNumberBack();
void onPasswordBack();
void onConfirmCodeBack();
void onSignupRequestSuccess(boolean resendingCode);
void onSignupRequestFailed(String errorMessage);
void onTokenCreationFailed();
void onUnlockSuccess();
void onUnlockError(String errorMessage);
void showTermsAndConditions();
void hideTermsAndConditions();
}
我的想法是为每个项目单元进行一个单元测试,因此对于每个视图和演示者实现,我需要一个单元测试,所以我想用roboletric对我的片段进行单元测试,例如,我想测试如果我单击“下一步”按钮,并且电子邮件是正确的,那么宿主活动的onEmailValidated()
方法将被调用。这是我的测试课
public class SignupEmailViewTest {
private SignupActivity_NEW mActivity;
private SignupActivity_NEW mSpyActivity;
private FrSignup_email mFragment;
private FrSignup_email mSpyFragment;
private Context mContext;
@Before
public void setUp() {
final Context context = RuntimeEnvironment.application.getApplicationContext();
this.mContext = context;
mActivity = Robolectric.buildActivity(SignupActivity_NEW.class).create().visible().get();
mSpyActivity = spy(mActivity);
mFragment = FrSignup_email.newInstance("");
mSpyFragment =spy(mFragment);
mSpyActivity.getFragmentManager()
.beginTransaction()
.replace(R.id.signupNew_fragmentHolder, mSpyFragment)
.commit();
mSpyActivity.getFragmentManager().executePendingTransactions();
}
@Test
public void testEmailValidation() {
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblTitle).isShown());
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle).isShown());
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
assertTrue(((SuperLabel) mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle)).getText().equals(mContext.getString(R.string.email_empty)));
((EditText) mSpyActivity.findViewById(R.id.signup_step2_new_txtEmail)).setText("aaa@bbb.ccc");
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
verify(mSpyFragment).onDataValidated();
verify(mSpyActivity).onEmailValidated();
}
}
一切正常,只是最后一次验证,但它不起作用。请注意,前面的verify是有效的,因此肯定会调用onEmailValidated
除了这个具体案例,我还有一些观点需要讨论:
如果使用roboeletric我被迫使用一个活动来实例化一个片段,我如何能够完全隔离地测试该片段(这将是单元测试的目标)?我的意思是,如果我使用roblectric.setupActivity(MyActivity.class)
并且活动在某个地方实例化了一个片段,它将加载活动和片段,这很好,但是如果活动管理一个片段流呢?如何在不手动导航到第二个或第三个片段的情况下测试它?有人可以说使用虚拟活动并使用FragmentTestUtil.startFragment
,但是在片段的onAttach()
方法中实现了与父活动的桥接吗?是我走错了路还是这个问题还没有解决
谢谢事实上,您甚至不需要RoboeElectric来做这些测试 如果每个片段/活动实现不同的视图接口,则可以实现假视图并实例化这些视图,而不是活动/片段。通过这种方式,您可以进行单独的测试 如果您不想实现视图接口的所有方法,您可以只使用Mockito和stub来实现单元测试所需的方法
如果您需要示例代码,请告诉我 实际上,您甚至不需要RoboeElectric来做这些测试 如果每个片段/活动实现不同的视图接口,则可以实现假视图并实例化这些视图,而不是活动/片段。通过这种方式,您可以进行单独的测试 如果您不想实现视图接口的所有方法,您可以只使用Mockito和stub来实现单元测试所需的方法
如果您需要示例代码,请告诉我 实际上,我想测试已经编写好的实现,测试一个伪实现有什么意义呢?使用MVP,大多数“有趣”的逻辑都在演示者中,这就是我首先要测试的。通过实现假视图,您可以将注意力集中在演示者上。您在视图上创建存根方法以返回演示者期望的内容,测试演示者逻辑并验证是否调用了视图的XXX方法。Activity/Fragment只包含设置文本、背景等的逻辑,通常您不需要进行测试。实际上,我希望有两个测试套件:presenter测试(使用模拟视图),其中我测试行为是否正确(例如:应该调用的方法被调用)。这是一个我已经实现的测试,我没有任何问题。但我也希望进行视图测试:使用roboeltric的本地单元测试(例如:如果调用某个方法,则在某个标签上设置了一些文本)和使用espresso的仪器化测试,以测试应用程序导航流等。我现在将重点放在roboeltric上,因为它更快,我希望以后实现espresso测试实际上我想测试已经编写好的实现,测试一个假的实现有什么意义呢?使用MVP,大多数“有趣”的逻辑都在演示者中,这就是我首先要测试的。通过实现假视图,您可以将注意力集中在演示者上。您在视图上创建存根方法以返回演示者期望的内容,测试演示者逻辑并验证是否调用了视图的XXX方法。Activity/Fragment只包含设置文本、背景等的逻辑,通常您不需要进行测试。实际上,我希望有两个测试套件:presenter测试(使用模拟视图),其中我测试行为是否正确(例如:应该调用的方法被调用)。这是一个我已经实现的测试,我没有任何问题。但我也希望进行视图测试:使用roboeltric的本地单元测试(例如:如果调用某个方法,则在某个标签上设置了一些文本)和使用espresso的仪器化测试来测试应用程序导航流等。我现在将重点放在roboeltric上,因为它更快,我想稍后实施浓缩咖啡测试您是否调试了测试以便确认片段使用了spy activty对象?您是否调试了测试以便确认片段使用了spy activty对象?