Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android binding and JUnit testing of notification

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?

like image 246
WebDucer Avatar asked Oct 17 '22 10:10

WebDucer


1 Answers

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);
    }
}
like image 187
David Rawson Avatar answered Oct 21 '22 05:10

David Rawson