Android单元测试片段在MPV中的应用

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)

我正在使用经典的MVP方法重新发明我的应用程序。为此,我阅读了许多文章和教程,我得出的结论是最好的方法是:

  • 为演示者和视图创建一个界面
  • 使片段和活动实现视图接口
  • 创建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对象?