简体   繁体   中英

Unit testing a RecyclerView Adapter throws NullPointerException when accessing mObservables

My view model holds a very simple recyclerview adapter

When I try to send it messages (which in turn calls notifyDatasetChanged ) it throws an exception like so

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

the problem is that the mObservers variable from AdapterDataObservable is null

the thing is that this extends Observable<AdapterDataObserver> which in turn defines mObservers as

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

so basically the moment my adapter is instantiated, it will call

private final AdapterDataObservable mObservable = new AdapterDataObservable();

(which is called by the way, mObservable is not null)

which in turn should call mObservers = new ArrayList<T>();

can someone explain why this is never called? or if there is a way to get past this problem?

as a side note the adapter is not mocked it is a solid object.

Edit:

Here is the code of the tests I'm using:

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

And below is the code that is tested:

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

I fixed mine by spying on the adapter and stubbing notifyDataSetChanged .

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

Please, note that changeItems calls notifyDataSetChanged internally.

i don't know if you have already found a solution or not, but this is for other people like me who ran into a similar problem:

make the test an android test (aka instrumented test ) and not a unit test.

while i cannot fully explain why, it seems that when notifying an adapter about a change ( notifyItemChanged() , notifyDataSetChanged() etc.) something about the inner logic of android requires an actual RecyclerView /adapter to receive the message.

once i moved my test from the Test folder to the AndroidTest folder, the problem was fixed.

PS

make sure to remove your old build configuration! android studio keeps referring to the old one (in the Test folder) and if you don't remove it you will receive a classNotFound error

I solved my problem doing this:

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

Where the AppTest is am empty class.

in my case, I use @RunWith(MockitoJUnitRunner.class) instrad of @RunWith(JUnit4.class) and work fine for me... the sample code:

Inside viewModel.fetchPokemonList() function I use 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);
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM