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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With