Android通知的绑定和JUnit测试

5

我想测试我的Android视图模型,特别是当setter是否应该通知更改时。

视图模型如下所示(具有更多可绑定属性):

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);
  }
}

在我的单元测试中,我注册了一个监听器来接收通知,并使用以下类对其进行跟踪:
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;
  }
}

所以我的测试方法看起来像下面这样:

@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));
}

如果我只执行测试方法,这种方法效果很好。如果我一次性执行所有测试,有些测试会失败,因为通知被调用的次数是0。我认为,在回调处理之前,断言已经被调用了。
我已经尝试过以下方法:
(1) 用Mockito模拟监听器,如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) 尝试使用多个SO答案中描述的CountDownLatch

但是它们都没有帮助我。我该怎么做才能测试绑定通知呢?


有没有人在Android上对视图模型进行单元测试? - WebDucer
你是否考虑授予悬赏?我认为它解决了你的问题(请参见链接的 GitHub 存储库),而且我花了很长时间回答。 - David Rawson
赏金已分配 - WebDucer
1个回答

3
您的测试是在示例项目中作为本地单元测试工作的(链接到GitHub存储库这里)。我无法重现您所遇到的错误。下面是一个完整的测试类的粗略工作示例,包括导入 - 它生成了您的EditViewModel的100%代码覆盖率:
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);
    }
}

为了诊断您遇到的问题,请确保您具有以下正确的依赖项:
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    testCompile "org.mockito:mockito-core:+"

    androidTestCompile "org.mockito:mockito-android:+"       
}
mockitodexmaker的旧设置已不再有效。可以在Maven Central上找到最新版本的Mockito。
此外,请确保您正在test中编写本地单元测试,而不是在androidTest中编写仪器化测试-请参见此问题以了解区别。
此外,使用复杂逻辑的ViewModel这样的场景通常通过提取帮助对象并将对象作为构造函数传递给ViewModel的依赖项进行测试,而不是直接对ViewModel本身进行测试,效果更好。
这可能看起来违反直觉,因为我们习惯于将Model视为没有依赖关系的数据对象。但是,ViewModel不仅仅是一个Model - 通常它还会负责将模型转换为视图和视图转换为模型,如此处所述。
假设您已经在项目的其他地方针对其进行了单元测试的类MyDateFormat。现在,您可以编写一个依赖于它的ViewModel,如下所示:
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);
    }
}

谢谢。我会尝试提取一个小版本来重现问题。我需要通知测试,因为一些setter会通知更多关于变化的信息(计算属性)。但奇怪的是,在编写这些“更复杂”的属性的测试之前,我的行为就开始了。 - WebDucer
@WebDucer 感谢您确认答案 - 如果您能够使用MCVE重现问题,我们可以查看一下。 - David Rawson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接