【问题标题】:Unit testing a RecyclerView Adapter throws NullPointerException when accessing mObservables访问 mObservable 时对 RecyclerView 适配器进行单元测试会引发 NullPointerException
【发布时间】:2019-10-01 08:38:36
【问题描述】:

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

当我尝试向它发送消息(这反过来又调用notifyDatasetChanged)时,它会抛出这样的异常

java.lang.NullPointerException
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:11996)
at

问题在于AdapterDataObservable 中的mObservers 变量为空

问题是这扩展了Observable<AdapterDataObserver>,而mObservers又定义为

protected final ArrayList<T> mObservers = new ArrayList<T>();

所以基本上在我的适配器被实例化的那一刻,它就会调用

private final AdapterDataObservable mObservable = new AdapterDataObservable();

(顺便调用,mObservable不为空)

反过来应该调用mObservers = new ArrayList&lt;T&gt;();

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

顺便说一句,适配器没有被模拟,它是一个实体对象。

编辑:

这是我正在使用的测试代码:

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

【问题讨论】:

  • 你的意思是android单元测试?不,不是,它是一个普通的单元测试,junit 测试,或者它们被称为
  • 这个问题根本没有任何测试,虽然它可能是一个仪器测试。
  • 我现在添加了显示此行为的测试的确切部分
  • Viewmodels 应该为我们提供标准化,但您的构造函数中的参数比构造函数所需的要多,并且持有对视图的引用,适配器应该在视图中,并且视图模型应该将数据转发给它.此外,测试适配器通常使用 Espresso 完成,测试 http 请求应该是 2 个不同的测试。
  • 我使用了这么多参数,因为我可以访问用于设置、api 调用、协程调度程序和日志记录的接口(否则可以使用我还没有足够练习的 Dagger 来完成)。我在视图模型中使用适配器,因为它们允许我更好地控制回收站中显示的内容,我可以直接添加/删除/更新项目,如果附加了它们,它们将更新各自的视图,否则它们什么也不做。使用 livedata 更新适配器意味着整个适配器总是得到更新,而不仅仅是更改

标签: android unit-testing android-recyclerview


【解决方案1】:

我通过监视适配器和存根 notifyDataSetChanged 修复了我的问题。

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

请注意,changeItems 在内部调用 notifyDataSetChanged

【讨论】:

  • 显然 notifyDataSetChanged() 在 Java 中返回 void,因此在 kotlin 中返回 Unit 并没有帮助,至少我们的 recyclerview 相关依赖仍然是基于 Java 的,所以它是这样的。
【解决方案2】:

我不知道你是否已经找到了解决方案,但这是为像我这样遇到类似问题的其他人准备的:

使测试成为android 测试(又名仪器测试)并且不是单元测试。

虽然我无法完全解释原因,但似乎在通知适配器有关更改(notifyItemChanged()notifyDataSetChanged() 等)时,有关 android 内部逻辑的某些内容需要实际的 RecyclerView/adapter 才能接收消息.

一旦我将测试从 Test 文件夹移动到 AndroidTest 文件夹,问题就解决了。

附言

确保删除旧的构建配置! android studio 一直引用旧的(在Test 文件夹中),如果您不删除它,您将收到classNotFound 错误

【讨论】:

  • 不,我没有找到解决方案,我采用了不同的方式(iirc 通过注入在构造函数上模拟的适配器),我知道它适用于 android 测试,但我想使用视图模型的单元测试,因为它们更快
  • @Cruces 可以帮助您了解如何通过单元测试解决问题
  • 我的 viewModel 变成了class LoginViewModel(private val settings: ISettings, private val service: AppService, application: Application, l: ILog, dispatchers: IDispatchers, val permissionChecker: (String) -&gt; Boolean, val adapter:StepAdapter) : BaseViewModel(application, l, dispatchers),当我开始测试时,我模拟了适配器并通过构造函数注入它
【解决方案3】:

我这样做解决了我的问题:

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

AppTest 在哪里是空类。

【讨论】:

    【解决方案4】:

    在我的情况下,我使用 @RunWith(MockitoJUnitRunner.class) instrad 的 @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);
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-12
      相关资源
      最近更新 更多