Hot questions for Using Mockito in android mvp
Question:
I'm trying to unit test my Presenter class using Mockito and I always end up failing the test:
org.mockito.exceptions.base.MockitoException: Cannot mock/spy class java.lang.String Mockito cannot mock/spy following: - final classes - anonymous classes - primitive types
This is what my Presenter class looks like:
public class EditorPresenter implements EditorContract.Presenter { private DataSource dataSourceImpl; private EditorContract.View mView; private SharedPreferences prefs; EditorPresenter(SharedPreferences prefs, DataSourceImpl dataSourceImpl, EditorContract.View mView) { this.dataSourceImpl = dataSourceImpl; this.mView = mView; this.prefs = prefs; mView.setPresenter(this); } @Override public void showNewNote() { String noteColor = prefs.getString("default_note_color", "#ef5350"); String textColor = prefs.getString("default_text_color", "#000000"); mView.noteColor(Color.parseColor(noteColor)); mView.textColor(Color.parseColor(textColor)); } }
And this is what I've done so far in EditorPresenterTest class:
public class EditorPresenterTest { @Mock private EditorContract.View mView; @Mock private DataSourceImpl mDataSourceImpl; @Mock private SharedPreferences sharedPrefs; @Mock private String noteColor; @Mock private String textColor; @Before public void setUpEditorPresenter() { MockitoAnnotations.initMocks(this); } @Test public void createEditorPresenter_newNote() { EditorPresenter editorPresenter = new EditorPresenter( sharedPrefs, mDataSourceImpl, mView); verify(mView).setPresenter(editorPresenter); } @Test public void showNewNote() { when(sharedPrefs.getString(eq("default_note_color"), eq("#ef5350"))).thenReturn(noteColor); when(sharedPrefs.getString(eq("default_text_color"), eq("#000000"))).thenReturn(textColor); verify(mView).textColor(Color.parseColor(noteColor)); verify(mView).noteColor(Color.parseColor(textColor)); }
(Note: I'm new to Mockito and testing)
I have passed the createEditorPresenter_newNote()
but the showNewNote()
failed the test and shows error. Any Feedback/Answers are welcome. Hope someone helps me. Thanks!
Answer:
I will first answer the exact question that you asked here but keep in mind that you have a larger issue that is hiding behind your compilation error, for which I will provide an answer right after. (please keep in mind that I have no real experience with Android, so exact class names and use cases might not be valid, but your issues are more with understanding what test frameworks do and not syntax-oriented).
Your first issue is that you are trying to create mock types of the String class, which is final. As you can see in the error from Mockito:
Mockito cannot mock/spy following:
- final classes
In essence, there is no real reason for creating a mock of a String, because you are not testing String functionality. You can just use a constant.
if that is what you wish to fix, just remove the @Mock
annotations from the noteColor
and textColor
variables and initialize them with some constant values.
More about testing frameworks and the other problems you are facing:
There is another major issue in your test case, and that is that you are trying to use the EditorPresenter
you created in the first test inside the scope of the second test.
The thing is that test frameworks run different tests in separate states (rightfully so). So when you create the EditorPresenter
instance inside the createEditorPresenter_newNote
method, it won't be visible for you in the showNewNote
test method, because it is a different process (not a different CPU process - just a process in the simple day-to-day term of the word).
What should you be doing instead?
That's what the before
method is for: it will be called before every test runs, so you can set up shared functionality in one place.
what you should be doing is more on the line of this:
public class EditorPresenterTest { @Mock private EditorContract.View mView; @Mock private DataSourceImpl mDataSourceImpl; @Mock private SharedPreferences sharedPrefs; private EditorPresenter editorPresenter; @Before public void setUpEditorPresenter() { MockitoAnnotations.initMocks(this); this.editorPresenter = new EditorPresenter( sharedPrefs, mDataSourceImpl, mView); } @Test public void createEditorPresenter_newNote() { verify(mView).setPresenter(editorPresenter); } @Test public void showNewNote() { editorPresenter.showNewNote(); String noteColor = "#ef5350"; // or whatever color you want String textColor = "#000000"; // or whatever color you want when(sharedPrefs.getString(eq("default_note_color"), eq("#ef5350"))).thenReturn(noteColor); when(sharedPrefs.getString(eq("default_text_color"), eq("#000000"))).thenReturn(textColor); verify(mView).textColor(Color.parseColor(noteColor)); verify(mView).noteColor(Color.parseColor(textColor)); } }
Question:
I'm trying to test my app with Mockito. It is built using MVP pattern. This is my Contract:
public interface CitiesContract { interface View { void addCitiesToList(List<City> cityList); } interface Presenter { void passCityListToView(); } interface Model { List<City> getCityList(); } }
This is my View:
public class CitiesActivity extends AppCompatActivity implements CitiesContract.View { private List<City> cityList = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cities); CitiesPresenter presenter = new CitiesPresenter(this); presenter.passCityListToView(); } @Override public void addCitiesToList(List<City> cities) { cityList.addAll(cities); } }
This is my Presenter:
public class CitiesPresenter implements CitiesContract.Presenter { private CitiesContract.View view; private CitiesModel model; public CitiesPresenter(CitiesContract.View view) { this.view = view; model = new CitiesModel(); } @Override public void passCityListToView() { List<City> cityList = model.getCityList(); view.addCitiesToList(cityList); } }
This is my Model:
public class CitiesModel implements CitiesContract.Model { @Override public List<City> getCityList() { List<City> cityList = new ArrayList<>(); //Add 30 cities to the list return cityList; } }
How can I test the passCityListToView()
method within my Presenter? This is what I have tried so far:
public class CitiesPresenterTest { private CitiesContract.Presenter citiesPresenter; @Mock private CitiesContract.View citiesView; @Mock private CitiesContract.Model citiesModel; public void setUp() { MockitoAnnotations.initMocks(this); citiesPresenter = new CitiesPresenter(citiesView); citiesModel = new CitiesModel(); } @Test public void testCityListIfNull() { when(citiesModel.getCityList()).thenReturn(null); citiesPresenter.passCityListToView(); verify(citiesView).addCitiesToList(null); } }
But I get NullPointerException
pointing to this line:
when(citiesModel.getCityList()).thenReturn(null);
How can I successfully pass this test? Thanks in advance.
Edit:
As requested, this is my logcat:
java.lang.NullPointerException at com.example.CitiesPresenterTest.testCityListIfNull(CitiesPresenterTest.java:28) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Answer:
Multiple things wrong.
First:
@Mock private CitiesContract.Model citiesModel;
that is basically undone by:
citiesModel = new CitiesModel();
The annotation alone is enough to create a mocked object (to be precise: the annotation together with that initMocks()
call).
But you then come in and create a real object, which you then try to add a specification for. But when this here is invoked:
when(citiesModel.getCityList()).thenReturn(null);
as said, citiesModel
isn't a Mockito created mock, but a real object of your production class.
That can't work. So, start by removing citiesModel = new CitiesModel();
from your code.
Next:
You do model = new CitiesModel();
in your presenter class. But there is no magic that would somehow put your mocked model instance into that field of the presenter class.
In other words: just declaring a mock alone isn't sufficient. You have to insure that it gets injected into your class under test. Either by passing it using a "test only" constructor (that takes more arguments) or by using the @InjectMocks annotation.
Question:
Hello i try to use mockito, to verify user password if invalid i want to verify it show error message.
But i got this following error :
Wanted but not invoked: loginView.showPasswordError(); -> at android.fanjavaid.com.tdd_android_login.LoginActivityTest.invalidPassword_notLoggedIn_showPasswordError(LoginActivityTest.java:84) However, there was exactly 1 interaction with this mock: loginView.showEmailError(); -> at android.fanjavaid.com.tdd_android_login.presenter.LoginPresenterImpl.validateInput(LoginPresenterImpl.java:23)
Here is my test method :
@Test public void invalidEmail_notLoggedIn_showEmailError() { LoginPresenter presenter = new LoginPresenterImpl(loginView, validator); presenter.validateInput(user); verify(loginView).showEmailError(); } @Test public void invalidPassword_notLoggedIn_showPasswordError() { when(user.getEmailAddress()).thenReturn("fanjavaid@gmail.com"); LoginPresenter presenter = new LoginPresenterImpl(loginView, validator); presenter.validateInput(user); verify(loginView).showPasswordError(); }
I already mock the email user in invalidPassword_notLoggedIn_showPasswordError()
with valid input, but i still get that error message.
Here is my Presenter implementation :
@Override public void validateInput(User user) { if (!validator.validateEmail(user.getEmailAddress())) { view.showEmailError(); } else if (validator.validatePassword(user.getPassword())) { view.showPasswordError(); } }
What i am missing for?
Thank you
Answer:
✔ ANSWER
After several minutes explore again, i found something interesting. I forgot to add mock to one class. Below i mock some classes moreless like this :
@RunWith(MockitoJUnitRunner.class) public class LoginActivityTest { @Mock User user; @Mock LoginView loginView; @Mock MyValidator validator; LoginPresenter presenter; @Before public void beforeTest() { presenter = new LoginPresenterImpl(loginView, validator); } ...
You can see that i mock validator class.
I got the error because in invalidPassword_notLoggedIn_showPasswordError()
method i didn't add mock value for email validation.
// Mock validation when(validator.validateEmail(user.getEmailAddress())).thenReturn(true);
If i don't mock it, it will ask about showEmailError()
but we just verify showPasswordError()
This cause my implementation using condition to check one by one, started from check email is valid or not, then password valid or not. If email doesn't exists and return value from validator doesn't exists the error will occured.
So i need to mock email address as valid and mock validator to return true(valid email).
That's my explanation and hope can help anyone who try mockito.
Question:
I've up until yesterday successfully put together a very readable Android project using the MVP-pattern and the Android Annotations library.
But yesterday when I started writing unittest for my LoginPresenter a problem has shown itself.
First some code from my LoginPresenter.
... @EBean public class LoginPresenterImpl implements LoginPresenter, LoginInteractor.OnLoginFinishedListener { @RootContext protected LoginActivity loginView; @Bean(LoginInteractorImpl.class) LoginInteractor loginInteractor; @Override public void validateCredentials(String username, String password) { if (loginView != null) { loginView.showProgress(); } if (TextUtils.isEmpty(username)) { // Check that username isn't empty onUsernameError(); } if (TextUtils.isEmpty(password)){ // Check that password isn't empty onPasswordError(); // No reason to continue to do login } else { } } @UiThread(propagation = UiThread.Propagation.REUSE) @Override public void onUsernameError() { if (loginView != null) { loginView.setUsernameError(); loginView.hideProgress(); } } ...
My test:
@RunWith(MockitoJUnitRunner.class) public class LoginPresenterImplTest { private LoginPresenter loginPresenter; @Mock private LoginPresenter.View loginView; @Before public void setUp() { // mock or create a Context object Context context = new MockContext(); loginPresenter = LoginPresenterImpl_.getInstance_(context); MockitoAnnotations.initMocks(this); } @After public void tearDown() throws Exception { loginPresenter = null; } @Test public void whenUserNameIsEmptyShowUsernameError() throws Exception { loginPresenter.validateCredentials("", "testtest"); // verify(loginPresenter).onUsernameError(); verify(loginView).setUsernameError(); } }
The problem is I've not used the standard approach of using MVP-pattern but instead trying out Android Annotations to make the code more readable. So I've not used attachView()- or detachView()-methods for attaching my presenter to my LoginActivity (view). This means that I can't mock my "view". Does someone know a workaround for this problem. I keep getting following message when running the test:
Wanted but not invoked: loginView.setUsernameError(); -> at com.conhea.smartgfr.login.LoginPresenterImplTest.whenUserNameIsEmptyShowUsernameError(LoginPresenterImplTest.java:48) Actually, there were zero interactions with this mock.
Solution (I'm not using @RootContext anymore):
Presenter:
@EBean public class LoginPresenterImpl extends AbstractPresenter<LoginPresenter.View> implements LoginPresenter, LoginInteractor.OnLoginFinishedListener { private static final String TAG = LoginPresenterImpl.class.getSimpleName(); @StringRes(R.string.activity_login_authenticating) String mAuthenticatingString; @StringRes(R.string.activity_login_aborting) String mAbortingString; @StringRes(R.string.activity_login_invalid_login) String mInvalidCredentialsString; @StringRes(R.string.activity_login_aborted) String mAbortedString; @Inject LoginInteractor mLoginInteractor; @Override protected void initializeDagger() { Log.d(TAG, "Initializing Dagger injection"); Log.d(TAG, "Application is :" + getApp().getClass().getSimpleName()); Log.d(TAG, "Component is: " + getApp().getComponent().getClass().getSimpleName()); Log.d(TAG, "UserRepo is: " + getApp().getComponent().userRepository().toString()); mLoginInteractor = getApp().getComponent().loginInteractor(); Log.d(TAG, "LoginInteractor is: " + mLoginInteractor.getClass().getSimpleName()); } @Override public void validateCredentials(String username, String password) { boolean error = false; if (!isConnected()) { noNetworkFailure(); error = true; } if (TextUtils.isEmpty(username.trim())) { // Check that username isn't empty onUsernameError(); error = true; } if (TextUtils.isEmpty(password.trim())) { // Check that password isn't empty onPasswordError(); error = true; } if (!error) { getView().showProgress(mAuthenticatingString); mLoginInteractor.login(username, password, this); } } ...
My tests (some of them):
@RunWith(AppRobolectricRunner.class) @Config(constants = BuildConfig.class) public class LoginPresenterImplTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); private LoginPresenterImpl_ mLoginPresenter; @Mock private LoginPresenter.View mLoginViewMock; @Mock private LoginInteractor mLoginInteractorMock; @Captor private ArgumentCaptor<LoginInteractor.OnLoginFinishedListener> mCaptor; @Before public void setUp() { mLoginPresenter = LoginPresenterImpl_.getInstance_(RuntimeEnvironment.application); mLoginPresenter.attachView(mLoginViewMock); mLoginPresenter.mLoginInteractor = mLoginInteractorMock; } @After public void tearDown() throws Exception { mLoginPresenter.detachView(); mLoginPresenter = null; } @Test public void whenUsernameAndPasswordIsValid_shouldLogin() throws Exception { String authToken = "Success"; mLoginPresenter.validateCredentials("test", "testtest"); verify(mLoginInteractorMock, times(1)).login( anyString(), anyString(), mCaptor.capture()); mCaptor.getValue().onSuccess(authToken); verify(mLoginViewMock, times(1)).loginSuccess(authToken); verify(mLoginViewMock, times(1)).hideProgress(); } @Test public void whenUsernameIsEmpty_shouldShowUsernameError() throws Exception { mLoginPresenter.validateCredentials("", "testtest"); verify(mLoginViewMock, times(1)).setUsernameError(); verify(mLoginViewMock, never()).setPasswordError(); verify(mLoginViewMock, never()).hideProgress(); } ...
Answer:
As a workaround you can have this:
public class LoginPresenterImpl ... { ... @VisibleForTesting public void setLoginPresenter(LoginPresenter.View loginView) { this.loginView = loginView; } }
In test class:
@Before public void setUp() { ... MockitoAnnotations.initMocks(this); loginPresenter.setLoginPresenter(loginView); }
But, as a rule of thumb, when you see @VisibleForTesting
annotation, that means you have ill architecture. Better to refactor your project.