I want to test my Android view models. Especially when the setter should notify about changes or not.
The view model looks like this (with more bindable properties):
public class EditViewModel extends BaseObservable {
private String _comment;
#Bindable
public String getComment() {
return _comment;
}
public void setComment(String comment) {
if (_comment == null && comment == null) {
// No changes, both NULL
return;
}
if (_comment != null && comment != null && _comment.equals(comment)) {
//No changes, both equals
return;
}
_comment = comment;
// Notification of change
notifyPropertyChanged(BR.comment);
}
}
In my UnitTest I register a listener, to get the notifications and track them with a following class:
public class TestCounter {
private int _counter = 0;
private int _fieldId = -1;
public void increment(){
_counter++;
}
public int getCounter(){
return _counter;
}
public void setFieldId(int fieldId){
_fieldId = fieldId;
}
public int getFieldId(){
return _fieldId;
}
}
So my test methods looks like the following:
#Test
public void setComment_RaisePropertyChange() {
// Arrange
EditViewModel sut = new EditViewModel(null);
sut.setComment("One");
final TestCounter pauseCounter = new TestCounter();
// -- Change listener
sut.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
#Override
public void onPropertyChanged(Observable sender, int propertyId) {
pauseCounter.increment();
pauseCounter.setFieldId(propertyId);
}
});
String newComment = "two";
// Act
sut.setComment(newComment);
// Assert
assertThat(pauseCounter.getCounter(), is(1));
assertThat(pauseCounter.getFieldId(), is(BR.comment));
assertThat(sut.getComment(), is(newComment));
}
If I execute the test methods alone, this approach works well. If I execute all tests at one, some fail, that the notification was 0 times called. I think, that the assertions is called before the callback could be handled.
I tried already following approches:
(1) Mock the listener with mockito as described in https://fernandocejas.com/2014/04/08/unit-testing-asynchronous-methods-with-mockito/.
#Test
public void setComment_RaisePropertyChange() {
// Arrange
EditViewModel sut = new EditViewModel(null);
sut.setComment("One");
Observable.OnPropertyChangedCallback listener = mock(Observable.OnPropertyChangedCallback.class);
// -- Change listener
sut.addOnPropertyChangedCallback(listener);
String newComment = "two";
// Act
sut.setComment(newComment);
// Assert
verify(listener, timeout(500).times(1)).onPropertyChanged(any(Observable.class), anyInt());
}
(2) Tried to use CountDownLatch as described in several SO answers.
None of them helped me. What can I do to be able to test the binding notification?
Your tests are working as a local unit tests in a sample project (link to GitHub repo here). I cannot reproduce the errors you are getting. A working rough example of a test class complete with imports is as follows - it generates 100% code coverage of your EditViewModel:
import android.databinding.Observable;
import org.junit.Test;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
public class EditViewModelTest {
#Test
public void setNewNonNullCommentRaisesPropertyChange() {
// Arrange
EditViewModel sut = new EditViewModel(null);
sut.setComment("One");
Observable.OnPropertyChangedCallback listener = mock(Observable.OnPropertyChangedCallback.class);
sut.addOnPropertyChangedCallback(listener);
String newComment = "two";
// Act
sut.setComment(newComment);
// Assert
verify(listener).onPropertyChanged(sut, BR.comment);
}
#Test
public void setNewNullCommentRaisesPropertyChange() {
// Arrange
EditViewModel sut = new EditViewModel(null);
sut.setComment("One");
Observable.OnPropertyChangedCallback listener = mock(Observable.OnPropertyChangedCallback.class);
sut.addOnPropertyChangedCallback(listener);
String newComment = null;
// Act
sut.setComment(newComment);
// Assert
verify(listener).onPropertyChanged(sut, BR.comment);
}
#Test
public void setEqualCommentDoesntRaisePropertyChange() {
// Arrange
EditViewModel sut = new EditViewModel(null);
sut.setComment("One");
Observable.OnPropertyChangedCallback listener = mock(Observable.OnPropertyChangedCallback.class);
sut.addOnPropertyChangedCallback(listener);
String newComment = "One";
// Act
sut.setComment(newComment);
// Assert
verify(listener, never()).onPropertyChanged(sut, BR.comment);
}
#Test
public void setNullToNullDoesntRaisePropertyChange() {
// Arrange
EditViewModel sut = new EditViewModel(null);
sut.setComment(null);
Observable.OnPropertyChangedCallback listener = mock(Observable.OnPropertyChangedCallback.class);
sut.addOnPropertyChangedCallback(listener);
String newComment = null;
// Act
sut.setComment(newComment);
// Assert
verify(listener, never()).onPropertyChanged(sut, BR.comment);
}
}
To diagnose the problem you are having, please make sure you have the correct dependencies as below:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile "org.mockito:mockito-core:+"
androidTestCompile "org.mockito:mockito-android:+"
}
The older setup with mockito and dexmaker is no longer valid. The most current versions of Mockito can be found on Maven Central
Also please check you are writing a local unit test in test rather than an instrumented test in androidTest - see this question for the difference.
Furthermore, such scenarios with complex logic inside a ViewModel are often better tested by extracting a helper object, making the object a dependency for the ViewModel passed inside the constructor, and testing against the helper object rather than against the ViewModel itself.
This may seem counter-intuitive because we are trained to think of Models as data objects with no dependencies. However, a ViewModel is more than a mere Model - often it ends up taking responsibilities of conversion of both model to view and view to model as in the discussion here.
Assume you have a tested class MyDateFormat with a unit test against it somewhere else in your project. You can now write a ViewModel that depends on it like this:
public class UserViewModel extends BaseObservable {
private final MyDateFormat myDateFormat;
#Nullable private String name;
#Nullable private Date birthDate;
public ProfileViewModel(#NonNull MyDateFormat myDateFormat) {
this.myDateFormat = myDateFormat;
}
#Bindable
#Nullable
public String getName() {
return name;
}
#Bindable
#Nullable
public String getBirthDate() {
return birthDate == null ? null : myDateFormat.format(birthDate.toDate());
}
public void setName(#Nullable String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
public void setBirthDate(#Nullable Date birthDate) {
this.birthDate = birthDate;
notifyPropertyChanged(BR.birthDate);
}
}
Related
In my app, I have a ViewModel looks like that:
public class MyExampleViewModel {
private LiveData<MyEntity> myLiveData;
#Inject
MyRepository myRepository;
#Inject
public MyExampleViewModel() {
}
public void init(final Long id) {
if (this.myLiveData == null) {
this.myLiveData = myRepository.getById(id);
}
}
public void toggleStar() NullPointerException {
final MyEntity myValue = this.myLiveData.getValue();
myValue.setStar(!myValue.getStar());
myRepository.save(myValue);
}
}
Also the code of MyRepository#getById (myDao is a room DAO and it is injected):
public LiveData<MyEntity> getById(final Long id) {
return myDao.getById(id);
}
The code of MyDao#getById:
#Query(
"SELECT * FROM myTable WHERE id=:id"
)
LiveData<MyEntity> getById(final Long id);
I also try to test this ViewModel using
myExampleViewModel.init(myId);
myExampleViewModel.toggleStar();
but after the init call my LiveData value is always null.
My first question is: is it a best practice to use getValue() on my LiveData or should I use Transformation.map?
My second question is: in my test, how can I have a LiveData populated? I tried to use CountingTaskExecutorRule and InstantTaskExecutorRule but without any success.
Thank you for your help!
I understood why myLiveData is not populated in my test. According to the documentation "LiveData objects that are lazily calculated on demand." and LiveData#getValue only get the value if the LiveData is already populated but doesn't calculate the value.
So I fixed my test adding a getter on my LiveData and an observer on my LiveData to force the calculation like that LiveDataUtil.getValue(myExampleViewModel.getMyLiveData()); with LiveDataUtil#getValue:
public class LiveDataUtil {
public static <T> T getValue(final LiveData<T> liveData) throws InterruptedException {
final Object[] data = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
#Override
public void onChanged(#Nullable T o) {
data[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
new Handler(Looper.getMainLooper()).post(() -> liveData.observeForever(observer));
latch.await(2, TimeUnit.SECONDS);
//noinspection unchecked
return (T) data[0];
}
}
After this fix, MyExampleViewModel class looks like:
public class MyExampleViewModel {
private LiveData<MyEntity> myLiveData;
#Inject
MyRepository myRepository;
#Inject
public MyExampleViewModel() {
}
public void init(final Long id) {
if (this.myLiveData == null) {
this.myLiveData = myRepository.getById(id);
}
}
public void toggleStar() NullPointerException {
final MyEntity myValue = this.myLiveData.getValue();
myValue.setStar(!myValue.getStar());
myRepository.save(myValue);
}
public LiveData<MyEntity> getMyLiveData() {
return myLiveData;
}
}
And my test method:
myExampleViewModel.init(myId);
LiveDataUtil.getValue(myExampleViewModel.getMyLiveData());
myExampleViewModel.toggleStar();
I fixed my test but I still don't know if using LiveData.getValue is a best practice and I found few documentation on this topic. So, I'm interested in this topic if you have more information.
It is a bit tough the explain the situation actually. I mock a class and pass it to another class's constructor. Then, I create the first class and call the first class's method under the second class's method, it returns null. Please check below:
class A {
public String getName() {
return "Something";
}
}
class B {
private A a;
public B(A insA) {
this.a = insA;
}
public String createName() {
return a.getName(); // when this is called, returns null.
}
}
class TestB {
public testBSomething() {
A mockA = mock(A.class);
when(mockA.getName()).thenReturn("Somevalue");
B insB = new B(mockA);
assertEqual("SomeValue", insB.createName()); // insB.createName() = null which should return "Somevalue"
}
}
I also tried doReturn, thenAnswer and spy variations, but no luck. Probably, I miss something, but I could not find. If anyone has any idea, I will really appreciate it. Thank you.
Here,
#RunWith(MockitoJUnitRunner.class)
public class TestB {
#Mock
A mockA;
#InjectMocks
B insB;
#Before
public void setup() {
insB = new B(mockA);
}
#Test
public void testBSomething() {
when(mockA.getName()).thenReturn("SomeValue");
Assert.assertEquals("SomeValue", insB.createName());
}
}
I am trying to write unit test cases for below class. Specially, for 'isSumOfTaskWeightIsValid()' method from the below. it has private member involved it. Could you please help writing test cases for that. I find it difficult because of the 'for loop' in that method where it loops over the 'mTasks'. Thanks in advance.
Class TaskCard {
private List<Integer> mTasks = new ArrayList<>();
private boolean mIsGood;
public TaskCard(boolean isGood) { mIsGood = isGood}
public void setUpListofTasks(DataBaseHelper db){
mTasks.addAll(db.getTasks());
}
public boolean isSumOfTaskWeightIsValid(){
int sum = 0;
for(int taskWeight : mTasks)
{ sum += taskWeight;
}
return (sum>0 || mIsGood);
}
}
You can use #Before annotation to fill your mTasks list and then you can call your isSumOfTaskWeightIsValid method. You also need set your mIsGood parameter in your constructor. Here is a sample test class.
private TaskCard taskCard;
#Before
public void initObjects() {
taskCard = new TaskCard(...); //True or False
//Initialize DataBaseHelper here
DataBaseHelper db = new DataBaseHelper();
taskCard.setUpListofTasks(db);
}
#Test
public void testIsSumOfTaskWeightIsValid() {
// Now your list is filled with the value you prove in #Before
assertTrue(taskCard.isSumOfTaskWeightIsValid());
}
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();
}
...
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.
Heads up to Developers that want to use Android Annotations in their project. Watch out when writing unittests that your code doesn't access the Android APIs. The underlying implementation of Android Annotations is heavily dependent on the Android APIs. So the code that is autogenerated could be dependent on this and make it difficult to write unittests.
Always remember that Android Annotations replaces your class with a final class that has an _ added at the end of it's classname. In this generated class a lot of boilerplate code is autogenerated depending on how the original class is annotated. In my case the problem is that I'm working on an Android-project and want a lot of my methods from my presenter to run on the UI-thread. This is achieved using Android Annotations using the #UIThread annotation. But this means that my method is actually wrapped with another method that calls the super-class:
#Override
public void onUsernameError() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
LoginPresenterImpl_.super.onUsernameError();
return;
}
UiThreadExecutor.runTask("", new Runnable() {
#Override
public void run() {
LoginPresenterImpl_.super.onUsernameError();
}
}
, 0L);
}
My testcase can't get past the line:
...
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
...
And that is of course because we don't have access to the Android APIs in a simple unittest. So in there lies the problem.
Conclusion: You have to be very careful when writing unittests for projects using Android Annotations, that the code that is autogenerated doesn't rely on Android related APIs.
It's the same problem when using androids TextUtil-class.
I have an app set up using Mortar/Flow and Dagger 2. It seems to work except for when I switch between two views of the same class. The new view ends up with the previous view's presenter.
For example, I have a ConversationScreen that takes a conversationId as a constructor argument. The first time I create a ConversationScreen and add it to Flow it creates the ConversationView which injects itself with a Presenter which is created with the conversationId that was passed to the screen. If I then create a new ConversationScreen with a different conversationId, when the ConversationView asks for a Presenter, Dagger returns the old Presenter, because the scope has not yet closed on the previous ConversationScreen.
Is there a way for me to manually close the scope of the previous screen before I set up the new one? Or have I just set up the scoping wrong to begin with?
ConversationView
public class ConversationView extends RelativeLayout {
#Inject
ConversationScreen.Presenter presenter;
public ConversationView(Context context, AttributeSet attrs) {
super(context, attrs);
DaggerService.<ConversationScreen.Component>getDaggerComponent(context).inject(this);
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
presenter.takeView(this);
}
#Override
protected void onDetachedFromWindow() {
presenter.dropView(this);
super.onDetachedFromWindow();
}
}
ConversationScreen
#Layout(R.layout.screen_conversation)
public class ConversationScreen extends Paths.ConversationPath implements ScreenComponentFactory<SomeComponent> {
public ConversationScreen(String conversationId) {
super(conversationId);
}
#Override
public String getTitle() {
title = Conversation.get(conversationId).getTitle();
}
#Override
public Object createComponent(SomeComponent parent) {
return DaggerConversationScreen_Component.builder()
.someComponent(parent)
.conversationModule(new ConversationModule())
.build();
}
#dagger.Component(
dependencies = SomeComponent.class,
modules = ConversationModule.class
)
#DaggerScope(Component.class)
public interface Component {
void inject(ConversationView conversationView);
}
#DaggerScope(Component.class)
#dagger.Module
public class ConversationModule {
#Provides
#DaggerScope(Component.class)
Presenter providePresenter() {
return new Presenter(conversationId);
}
}
#DaggerScope(Component.class)
static public class Presenter extends BasePresenter<ConversationView> {
private String conversationId;
#Inject
Presenter(String conversationId) {
this.conversationId = conversationId;
}
#Override
protected void onLoad(Bundle savedInstanceState) {
super.onLoad(savedInstanceState);
bindData();
}
void bindData() {
// Show the messages in the conversation
}
}
}
If you use the default ScreenScoper and PathContextFactory classes from Mortar/Flow example project, you will see that the name of the new scope to create is the name of the Screen class.
Because you want to navigate from one instance of ConversationScreen to another instance of ConversationScreen, the name of the new scope will be equal to the name of previous scope. Thus, you won't create a new Mortar scope but just reuse the previous one, which means reusing the same presenter.
What you need is to change the naming policy of the new scope. Rather than using only the name of the new screen class, add something else.
Easiest fix is to use the instance identifier: myScreen.toString().
Another better fix is to have a tracking of the screen/scope names.
Following example extracted from https://github.com/lukaspili/Mortar-architect
class EntryCounter {
private final SimpleArrayMap<Class, Integer> ids = new SimpleArrayMap<>();
int get(History.Entry entry) {
Class cls = entry.path.getClass();
return ids.containsKey(cls) ? ids.get(cls) : 0;
}
void increment(History.Entry entry) {
update(entry, true);
}
void decrement(History.Entry entry) {
update(entry, false);
}
private void update(History.Entry entry, boolean increment) {
Class cls = entry.path.getClass();
int id = ids.containsKey(cls) ? ids.get(cls) : 0;
ids.put(cls, id + (increment ? 1 : -1));
}
}
And then use this counter when creating new scope:
private ScopedEntry buildScopedEntry(History.Entry entry) {
String scopeName = String.format("ARCHITECT_SCOPE_%s_%d", entry.path.getClass().getName(), entryCounter.get(entry));
return new ScopedEntry(entry, MortarFactory.createScope(navigator.getScope(), entry.path, scopeName));
}
And in some other place, i'm incrementing/decrementing the counter if new scope is pushed or scope is detroyed.
The scope in ScreenScoper is based on a string, which if you create the same path, it will use the same name as it bases it on the class name of your path.
I solved this by removing some noise from the ScreenScoper, considering I'm not using #ModuleFactory in my Dagger2-driven project anyways.
public abstract class BasePath
extends Path {
public abstract int getLayout();
public abstract Object createComponent();
public abstract String getScopeName();
}
public class ScreenScoper {
public MortarScope getScreenScope(Context context, String name, Object screen) {
MortarScope parentScope = MortarScope.getScope(context);
return getScreenScope(parentScope, name, screen);
}
/**
* Finds or creates the scope for the given screen.
*/
public MortarScope getScreenScope(MortarScope parentScope, final String name, final Object screen) {
MortarScope childScope = parentScope.findChild(name);
if (childScope == null) {
BasePath basePath = (BasePath) screen;
childScope = parentScope.buildChild()
.withService(DaggerService.TAG, basePath.createComponent())
.build(name);
}
return childScope;
}
}