RecyclerView适配器单元测试在访问mObservables时抛出NullPointerException

12

我的视图模型持有一个非常简单的recyclerview适配器。

当我尝试发送消息(自然地调用notifyDatasetChanged),它会抛出以下异常:

java.lang.NullPointerException
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:11996)
at
问题在于来自AdapterDataObservable的mObservers变量为空。
问题出在这个类扩展了Observable,而这个类又将mObservers定义为。
protected final ArrayList<T> mObservers = new ArrayList<T>();

基本上,一旦我的适配器被实例化,它就会调用

(Tip: The original sentence is already concise and easy to understand, so it just needs to be translated into Chinese without changing the meaning.)
private final AdapterDataObservable mObservable = new AdapterDataObservable();

(顺便说一下,这被称为mObservable不为空)

它应该调用mObservers = new ArrayList<T>();

有人能解释为什么这从来没有被调用吗?或者是否有方法可以解决这个问题?

另外,适配器没有被模拟,它是一个实体对象。

编辑:

这里是我使用的测试代码:

class LoginViewModelTest {

     private lateinit var vm: LoginViewModel

        @get:Rule
        val rule = InstantTaskExecutorRule()

        @Before
        fun setUp() {

            whenever(settings.hasShownWelcome).thenReturn(false)
            whenever(settings.serverIp).thenReturn("http://127.0.0.1")

            //this is where the crash happens
            vm = LoginViewModel(settings, service, app, TestLog, TestDispatchers) { p -> permissionGranted }
        }

以下是被测试的代码:

class LoginViewModel(private val settings: ISettings, private val service: AppService, application: Application, l: ILog, dispatchers: IDispatchers, val permissionChecker: (String) -> Boolean) :  BaseViewModel(application, l, dispatchers)

    val stepAdapter :StepAdapter

    init {
        val maxSteps = calculateSteps()
        //after this assignment, during the normal run, the stepAdapter.mObservable.mObservers is an empty array
        //during unit tests, after this assignment it is null
        stepAdapter = StepAdapter(maxSteps) 
    }

1
你是指Android单元测试吗?不是的,这是普通的单元测试,JUnit测试,或者无论它们被称为什么。 - Cruces
这个问题根本没有任何测试,虽然它可能是一个带有仪器的测试。 - Martin Zeitler
1
我现在已经添加了测试的确切部分,显示了这种行为。 - Cruces
Viewmodels应该给我们提供标准化,但是你的构造函数有比需要更多的参数,并且持有对视图的引用,适配器应该在视图中,而viewmodel应该将数据转发给它。此外,测试适配器通常使用Espresso进行,而测试http请求应该是2个不同的测试。 - cutiko
我使用了很多参数,因为我提供了接口来进行设置、API调用、协程调度和日志记录(否则可以使用Dagger进行,但我还没有足够的实践经验)。我在视图模型中使用适配器,因为它们允许我更好地控制在可回收视图中显示的内容,我可以直接添加/删除/更新项目,并且如果它们已附加,则会更新其相应的视图,否则它们什么也不做。使用LiveData更新适配器意味着整个适配器始终会得到更新,而不仅仅是更改。 - Cruces
5个回答

4

我通过监听适配器并对notifyDataSetChanged进行桩测试来修复我的问题。

    val spyAdapter = spyk(adapter)
    every { spyAdapter.notifyDataSetChanged() } returns Unit
    spyAdapter.changeItems(items)
    verify { spyAdapter.notifyDataSetChanged() }

请注意,changeItems 内部调用了 notifyDataSetChanged

显然,在Java中,notifyDataSetChanged()返回void,因此在kotlin中返回Unit并没有帮助,至少我们的recyclerview相关依赖仍然基于Java,所以就是这样。 - AndroidRocks

1
我不知道你是否已经找到解决方案,但这是给像我一样遇到类似问题的其他人的建议:
将测试变成一个 Android 测试(也称为“instrumented test”),而不是单元测试。
虽然我无法完全解释为什么,但似乎当通知适配器有关更改的消息(notifyItemChanged()、notifyDataSetChanged() 等)时,Android 的内部逻辑需要实际的 RecyclerView/适配器来接收该消息。
一旦将我的测试从 Test 文件夹移动到 AndroidTest 文件夹中,问题就得到了解决。
附注:
确保删除旧的构建配置!Android Studio 会继续引用旧的配置(在 Test 文件夹中),如果你不删除它,就会收到 classNotFound 错误。

我没有找到解决方案,而是采用了不同的方法(如果我没记错的话,是通过在构造函数中注入被模拟的适配器),我知道这对Android测试很有用,但我想使用单元测试来测试视图模型,因为它们更快。 - Cruces
@Cruces 可以帮助理解你如何解决单元测试问题。 - Tejas Sherdiwala
我的viewModel变成了class LoginViewModel(private val settings: ISettings, private val service: AppService, application: Application, l: ILog, dispatchers: IDispatchers, val permissionChecker: (String) -> Boolean, val adapter:StepAdapter) : BaseViewModel(application, l, dispatchers),当我开始测试时,我会模拟适配器并通过构造函数注入它。 - Cruces

0
在我的情况下,我使用`@RunWith(MockitoJUnitRunner.class)`而不是`@RunWith(JUnit4.class)`,对我来说效果很好...示例代码如下:
在`viewModel.fetchPokemonList()`函数内部,我使用`adapter.notifyDatasetChanged()`。
@RunWith(MockitoJUnitRunner.class)
public class PokemonCardListTest {
    @Rule public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();

    @Mock Context context;
    @Mock LifecycleOwner lifecycleOwner;
    @Mock public List<PokemonCard> pokemonCardList;
    private Repository repository;
    private Lifecycle lifecycle;
    private PokemonCardListViewModel viewModel;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);


        pokemonCardList = Arrays.asList(
                new PokemonCard("1" ,"Card1" , "Artist1"),
                new PokemonCard("2" ,"Card2" , "Artist2")
        );

        repository = new FakeRepository(pokemonCardList);

        lifecycle = new LifecycleRegistry(lifecycleOwner);
        viewModel = new PokemonCardListViewModel(context , repository);

    }

    @Test
    public void testSearch() {

        viewModel.fetchPokemonList("99");
        assertEquals(viewModel.getAdapter().getValue().getItemCount() , pokemonCardList.size());
    
        viewModel.fetchPokemonList("0");
        assertEquals(viewModel.getAdapter().getValue().getItemCount() , 0);
    }

0

我通过以下方法解决了我的问题:

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.N, Build.VERSION_CODES.O], application = AppTest::class)
class HomeAdapterTest {

这里的AppTest是一个空类。


0

我也遇到了同样的问题。在研究RecyclerView的代码后,我实现了一种模拟mObservable的方法,这个问题就解决了。以下是一个欺骗适配器的示例(Kotlin):

val observersValueMock: ArrayList<AdapterDataObserver> = mock()
// Modifiers to set private final variables
val modifiersField = Field::class.java.getDeclaredField("modifiers")
modifiersField.isAccessible = true
// Get Recycler Adapter mObservable instance
val mObservableField = (generalAdapter::class.java.superclass as Class)
    .getDeclaredField("mObservable")
mObservableField.isAccessible = true
val observableValue = mObservableField.get(generalAdapter)

//observableValue is Observable<AdapterDataObserver>,
//which has variable mObservers of type ArrayList<AdapterDataObserver>
//Exactly 'mObservers' called when you call notifyDatasetChange. And 
//because you don't attach adapter to a real RecyclerView, it's null.
//Need to set an empty array or mock to get rid of NPE  
val observersField = observableValue.javaClass.superclass.getDeclaredField("mObservers")
observersField.isAccessible = true
//Allow to set final variables
modifiersField.setInt(observersField, observersField.modifiers and Modifier.FINAL.inv())
observersField.set(observableValue, observersValueMock)

希望这段代码能够帮助你,或者至少能够给你提供模拟所需内容的线索。

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