InjectView在单元测试中不起作用

时间:2015-03-03 19:36:28

标签: android android-studio gradle robolectric roboguice

tl; dr:在单元测试中,@Inject成功注入了所需的字段,但@InjectView将其保留为空,从而导致错误。 @InjectView适用于正常的应用程序运行。


我有一个使用RoboGuice的Android应用程序,我正在尝试单元测试。我正在尝试测试的其中一项活动如下:

@ContentView(R.layout.activity_login)
public class LoginActivity extends RoboActivity {

    /**
     * Our injected API class.
     */
    @Inject
    private Api apiConn;

    /**
     * Main application state class.
     */
    @Inject
    private Application appState;

    /**
     * Keep track of the login task to ensure we can cancel it if requested.
     */
    private UserLoginTask mAuthTask = null;

    /**
     * Logo header
     */
    @InjectView(R.id.logoHeader)
    private TextView mLogoHeader;

    /**
     * Signin button.
     */
    @InjectView(R.id.email_sign_in_button)
    private Button mEmailSignInButton;

    /**
     * Email text view.
     */
    @InjectView(R.id.email)
    private EditText mEmailView;

    /**
     * Password text view.
     */
    @InjectView(R.id.password)
    private EditText mPasswordView;

    /**
     * Login progress view.
     */
    @InjectView(R.id.login_progress)
    private View mProgressView;

    /**
     * Login form view.
     */
    @InjectView(R.id.login_form)
    private View mLoginFormView;

    /**
     * Fired when Activity has been requested.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Check if the server is actually online
        PingTask ping = new PingTask();
        ping.execute();

        // Check if the user is already logged in.
        if (appState.hasLoginToken() && appState.getLoginToken().isValid()) {
            // Hide everything!
            mLogoHeader.setVisibility(View.INVISIBLE);
            mEmailView.setVisibility(View.INVISIBLE);
            mPasswordView.setVisibility(View.INVISIBLE);
            mEmailSignInButton.setVisibility(View.INVISIBLE);

            // The user already has a token, let's validate it and pull in extra information.
            AuthenticateTask authenticate = new AuthenticateTask();
            authenticate.execute();
            return;
        }
        appState.clearSettings();

        // Set up the login form.
        mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
                if (id == R.id.login || id == EditorInfo.IME_NULL) {
                    attemptLogin();
                    return true;
                }
                return false;
            }
        });

        mEmailSignInButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                attemptLogin();
            }
        });
    }

    /**
     * Attempts to sign in or register the account specified by the login form.
     * If there are form errors (invalid email, missing fields, etc.), the
     * errors are presented and no actual login attempt is made.
     */
    void attemptLogin() {
        if (mAuthTask != null) {
            return;
        }

        // Reset errors.
        mEmailView.setError(null);
        mPasswordView.setError(null);

        // Store values at the time of the login attempt.
        String email = mEmailView.getText().toString();
        String password = mPasswordView.getText().toString();

        boolean cancel = false;
        View focusView = null;

        // Check for a valid password, if the user entered one.
        if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) {
            mPasswordView.setError(getString(R.string.error_invalid_password));
            focusView = mPasswordView;
            cancel = true;
        }

        // Check for a valid email address.
        if (TextUtils.isEmpty(email)) {
            mEmailView.setError(getString(R.string.error_field_required));
            focusView = mEmailView;
            cancel = true;
        } else if (!isEmailValid(email)) {
            mEmailView.setError(getString(R.string.error_invalid_email));
            focusView = mEmailView;
            cancel = true;
        }

        if (cancel) {
            // There was an error; don't attempt login and focus the first
            // form field with an error.
            focusView.requestFocus();
        } else {
            // Show a progress spinner, and kick off a background task to
            // perform the user login attempt.
            showProgress(true);
            mAuthTask = new UserLoginTask(email, password);
            mAuthTask.execute((Void) null);
        }
    }

    /**
     * Checks if a given email address is valid.
     *
     * @param email Email address to check.
     *
     * @return Whether the email is valid or not.
     */
    private boolean isEmailValid(String email) {
        String ePattern = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$";

        Pattern p = Pattern.compile(ePattern);
        Matcher m = p.matcher(email);

        return m.matches();
    }

    /**
     * Checks if a given password is valid.
     *
     * @param password Password to check.
     *
     * @return Whether the password is valid or not.
     */
    private boolean isPasswordValid(CharSequence password) {
        //TODO: Is this needed?
        return password.length() > 4;
    }

    /**
     * Shows the progress UI and hides the login form.
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    void showProgress(final boolean show) {
        // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
        // for very easy animations. If available, use these APIs to fade-in
        // the progress spinner.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
            int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime);

            mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
            mLoginFormView.animate().setDuration(shortAnimTime).alpha(
                    show ? 0 : 1).setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
                }
            });

            mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
            mProgressView.animate().setDuration(shortAnimTime).alpha(
                    show ? 1 : 0).setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
                }
            });
        } else {
            // The ViewPropertyAnimator APIs are not available, so simply show
            // and hide the relevant UI components.
            mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
            mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
        }
    }

    /**
     * Stop hardware back button functionality.
     */
    @Override
    public void onBackPressed() {
    }

    /**
     * User Login Task.
     */
    public class UserLoginTask extends AsyncTask<Void, Void, Boolean> {

        /**
         * Email to login with.
         */
        private final String mEmail;

        /**
         * Password to login with.
         */
        private final String mPassword;

        /**
         * Constructor.
         *
         * @param email    Email to login with.
         * @param password Password to login with.
         */
        public UserLoginTask(String email, String password) {
            mEmail = email;
            mPassword = password;
        }

        /**
         * Main task.
         *
         * @param params Parameters.
         *
         * @return Whether login was successful or not.
         */
        @Override
        protected Boolean doInBackground(Void... params) {
            JSONObject responseJson;
            boolean success;
            String token;
            JSONArray drivers;
            JSONArray moods;
            JSONArray vehicles;
            try {
                // Contact API.
                String response = apiConn.login(mEmail, mPassword);
                responseJson = new JSONObject(response);
                success = responseJson.getBoolean("success");
                JSONObject data = responseJson.getJSONObject("data");
                token = data.getString("token");
                drivers = data.getJSONArray("drivers");
                moods = data.getJSONArray("moods");
                vehicles = data.getJSONArray("vehicles");
            } catch (ApiException e) {
                return false;
            } catch (JSONException e) {
                return false;
            }

            if (!success) {
                return false;
            }

            // Persist data.
            appState.setLoginToken(token);
            appState.setDrivers(drivers);
            appState.setMoods(moods);
            appState.setVehicles(vehicles);
            return true;
        }

        /**
         * Fired after the task has executed.
         *
         * @param success Whether the task was successful.
         */
        @Override
        protected void onPostExecute(final Boolean success) {
            mAuthTask = null;
            showProgress(false);

            if (success) {
                // Start chooser activity
                Intent chooserIntent = new Intent(getApplicationContext(), ChooserActivity.class);
                startActivity(chooserIntent);

                // Close task.
                finish();
            } else {
                mPasswordView.setError(getString(R.string.error_incorrect_password));
                mPasswordView.requestFocus();
            }
        }

        /**
         * Fired when the task was cancelled.
         */
        @Override
        protected void onCancelled() {
            mAuthTask = null;
            showProgress(false);
        }

    }

    /**
     * Ping task to test if API is available.
     */
    public class PingTask extends AsyncTask<Void, Void, Boolean> {

        /**
         * Alert dialog.
         */
        private AlertDialog.Builder alertDialog;

        /**
         * Fired before executing task.
         */
        @Override
        protected void onPreExecute() {
            super.onPreExecute();

            alertDialog = new AlertDialog.Builder(LoginActivity.this);
        }

        /**
         * Main task.
         *
         * @param params Parameters.
         *
         * @return Whether the API is online or not.
         */
        @Override
        protected Boolean doInBackground(Void... params) {
            boolean webOnline = apiConn.isWebOnline();
            Log.d("API", String.valueOf(webOnline));

            return webOnline;
        }

        /**
         * Fired after task was executed.
         *
         * @param success Whether the task was successful.
         */
        @Override
        protected void onPostExecute(final Boolean success) {
            super.onPostExecute(success);

            Log.d("API", String.valueOf(success));
            if (!success) {
                alertDialog.setTitle(getString(R.string.cannot_connect));
                alertDialog.setMessage(getString(R.string.cannot_connect_long));

                AlertDialog dialog = alertDialog.create();
                dialog.show();
            }
        }
    }

    /**
     * Authenticate with existing token task.
     */
    class AuthenticateTask extends AsyncTask<Void, Void, Boolean> {

        /**
         * Array of drivers from Api.
         */
        private JSONArray drivers;

        /**
         * Array of moods from Api.
         */
        private JSONArray moods;

        /**
         * Array of vehicle from Api.
         */
        private JSONArray vehicles;

        /**
         * Main task.
         *
         * @param params Parameters.
         *
         * @return Whether the task was successful or not.
         */
        @Override
        protected Boolean doInBackground(Void... params) {
            JSONObject responseJson;
            boolean success;
            drivers = null;
            moods = null;
            vehicles = null;
            try {
                String response = apiConn.authenticate(appState.getLoginToken());
                responseJson = new JSONObject(response);
                success = responseJson.getBoolean("success");
                JSONObject data = responseJson.getJSONObject("data");
                drivers = data.getJSONArray("drivers");
                moods = data.getJSONArray("moods");
                vehicles = data.getJSONArray("vehicles");
            } catch (ApiException | JSONException e) {
                success = false;
            }

            return success;
        }

        /**
         * Fired after the task has been executed.
         *
         * @param success Whether the task was successful.
         */
        @Override
        protected void onPostExecute(Boolean success) {
            super.onPostExecute(success);

            if (!success) {
                Log.d("JWT", "Token Invalid");
                // Token is invalid! Clear and show login form.
                appState.clearSettings();

                // Show the form again.
                mLogoHeader.setVisibility(View.VISIBLE);
                mEmailView.setVisibility(View.VISIBLE);
                mPasswordView.setVisibility(View.VISIBLE);
                mEmailSignInButton.setVisibility(View.VISIBLE);

                return;
            }

            appState.setDrivers(drivers);
            appState.setMoods(moods);
            appState.setVehicles(vehicles);

            Toast toast = Toast.makeText(getApplicationContext(), getString(R.string.welcome_back), Toast.LENGTH_SHORT);
            toast.show();

            // Start chooser activity
            Intent chooserIntent = new Intent(LoginActivity.this, ChooserActivity.class);
            startActivity(chooserIntent);
        }
    }
}

问题是onCreate方法崩溃,说mPasswordView在此行为空:

mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {

事实证明,mPasswordView实际上是null。它应该由RoboGuice的@InjectView注释设置,当我在我的设备上使用该应用程序但不适用于单元测试时,它会起作用。 @Inject无论如何......

如何通过@InjectView进行单元测试以解析观看次数?


额外课程

这是我的LoginActivityTest文件:

@Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18, reportSdk = 18)
@RunWith(RobolectricGradleTestRunner.class)
public class LoginActivityTest {

    private final Api apiMock = mock(Api.class);

    private final Application appMock = mock(Application.class);

    private final ActivityController<LoginActivity> controller = buildActivity(LoginActivity.class);

    @Before
    public void setup() {
        RoboGuice.overrideApplicationInjector(Robolectric.application, new LoginTestModule());
    }

    @After
    public void teardown() {
        RoboGuice.Util.reset();
    }

    @Test
    public void createTriggersPing() {
        ActivityController controller = Robolectric.buildActivity(LoginActivity.class);
        controller.create();
        controller.start();
        verify(apiMock).isWebOnline();
    }

    private class LoginTestModule extends AbstractModule {

        @Override
        protected void configure() {
            bind(Api.class).toInstance(apiMock);
            bind(Application.class).toInstance(appMock);
        }
    }
}

和我的RobolectricGradleTestRunner课程:

public class RobolectricGradleTestRunner extends RobolectricTestRunner {

    public RobolectricGradleTestRunner(Class<?> testClass) throws org.junit.runners.model.InitializationError {
        super(testClass);
    }

    @Override
    protected AndroidManifest getAppManifest(Config config) {
        String manifestProperty = System.getProperty("android.manifest");
        if (config.manifest().equals(Config.DEFAULT) && manifestProperty != null) {
            String resProperty = System.getProperty("android.resources");
            String assetsProperty = System.getProperty("android.assets");

            return new AndroidManifest(Fs.fileFromPath(manifestProperty), Fs.fileFromPath(resProperty),
                    Fs.fileFromPath(assetsProperty));
        }
        return super.getAppManifest(config);
    }
}

1 个答案:

答案 0 :(得分:0)

事实证明这与Roboguice本身无关。 OP的设置存在一些特殊的错误配置。通过聊天解决了它。这些问题使用的是Roboguice的@ContentView(它显然不能很好地用于测试,在之前的版本中也是如此)和错误配置的XML文件。