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