Android 通知的绑定和 JUnit 测试
Android binding and JUnit testing of notification
我想测试我的 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);
}
}
在我的 UnitTest 中,我注册了一个侦听器,以获取通知并使用以下 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;
}
}
所以我的测试方法如下所示:
@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) 按照 https://fernandocejas.com/2014/04/08/unit-testing-asynchronous-methods-with-mockito/.
中的描述使用 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) 尝试使用 CountDownLatch
,如几个 SO 答案中所述。
None 他们帮助了我。我该怎么做才能测试绑定通知?
您的测试在示例项目(link 到 GitHub 存储库 here)中作为本地单元测试运行。我无法重现您遇到的错误。一个使用导入完成的测试 class 的粗略工作示例如下 - 它生成 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:+"
}
mockito
和 dexmaker
的旧设置不再有效。可以在 Maven Central
上找到最新版本的 Mockito
另请检查您是在 test
中编写本地单元测试,而不是在 androidTest
中编写仪器测试 - 请参阅 this question 了解差异。
此外,这种在 ViewModel
中具有复杂逻辑的场景通常可以通过提取辅助对象来更好地测试,使该对象成为构造函数内部传递的 ViewModel 的依赖项,并针对辅助对象进行测试而不是针对 ViewModel 本身。
这似乎有悖常理,因为我们习惯于将模型视为没有依赖关系的数据对象。然而,ViewModel 不仅仅是一个模型 - 通常它最终会负责将模型转换为视图以及将视图转换为模型,如讨论中所述 here.
假设您有一个经过测试的 class 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);
}
}
我想测试我的 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);
}
}
在我的 UnitTest 中,我注册了一个侦听器,以获取通知并使用以下 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;
}
}
所以我的测试方法如下所示:
@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) 按照 https://fernandocejas.com/2014/04/08/unit-testing-asynchronous-methods-with-mockito/.
中的描述使用 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) 尝试使用 CountDownLatch
,如几个 SO 答案中所述。
None 他们帮助了我。我该怎么做才能测试绑定通知?
您的测试在示例项目(link 到 GitHub 存储库 here)中作为本地单元测试运行。我无法重现您遇到的错误。一个使用导入完成的测试 class 的粗略工作示例如下 - 它生成 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);
}
}
要诊断您遇到的问题,请确保您具有如下正确的依赖关系:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile "org.mockito:mockito-core:+"
androidTestCompile "org.mockito:mockito-android:+"
}
mockito
和 dexmaker
的旧设置不再有效。可以在 Maven Central
另请检查您是在 test
中编写本地单元测试,而不是在 androidTest
中编写仪器测试 - 请参阅 this question 了解差异。
此外,这种在 ViewModel
中具有复杂逻辑的场景通常可以通过提取辅助对象来更好地测试,使该对象成为构造函数内部传递的 ViewModel 的依赖项,并针对辅助对象进行测试而不是针对 ViewModel 本身。
这似乎有悖常理,因为我们习惯于将模型视为没有依赖关系的数据对象。然而,ViewModel 不仅仅是一个模型 - 通常它最终会负责将模型转换为视图以及将视图转换为模型,如讨论中所述 here.
假设您有一个经过测试的 class 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);
}
}