mara122
mara122

Reputation: 323

NPE while unit testing

I'm trying to write unit test for my method called getBestSellers().

Here it is:

package bookstore.scraper.book.scrapingtypeservice;

import bookstore.scraper.enums.Bookstore;
import bookstore.scraper.book.Book;
import bookstore.scraper.fetcher.empik.EmpikFetchingBookService;
import bookstore.scraper.fetcher.merlin.MerlinFetchingBookService;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import bookstore.scraper.urlproperties.MerlinUrlProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import static bookstore.scraper.utilities.JSoupConnector.connect;

@Service
public class BestSellersService {

    private final EmpikUrlProperties empikUrlProperties;
    private final MerlinUrlProperties merlinUrlProperties;
    private final EmpikFetchingBookService empikBookService;
    private final MerlinFetchingBookService merlinBookService;

    @Autowired
    public BestSellersService(EmpikFetchingBookService empikBookService, MerlinFetchingBookService merlinBookService, EmpikUrlProperties empikUrlProperties, MerlinUrlProperties merlinUrlProperties) {
        this.empikBookService = empikBookService;
        this.merlinBookService = merlinBookService;
        this.empikUrlProperties = empikUrlProperties;
        this.merlinUrlProperties = merlinUrlProperties;
    }

    public Map<Bookstore, List<Book>> getBestSellers() {
        Map<Bookstore, List<Book>> bookstoreWithBestSellers = new EnumMap<>(Bookstore.class);

        bookstoreWithBestSellers.put(Bookstore.EMPIK, empikBookService
                .get5BestSellersEmpik(connect(empikUrlProperties.getEmpik().getBestSellers())));
        bookstoreWithBestSellers.put(Bookstore.MERLIN, merlinBookService
                .get5BestSellersMerlin(connect(merlinUrlProperties.getMerlin().getBestSellers())));

        return bookstoreWithBestSellers;
    }
}

So, first I prepared test which looks like this:

package bookstore.scraper.book.scrapingtypeservice;

import bookstore.scraper.book.Book;
import bookstore.scraper.dataprovider.EmpikBookProvider;
import bookstore.scraper.dataprovider.MerlinBookProvider;
import bookstore.scraper.enums.Bookstore;
import bookstore.scraper.fetcher.empik.EmpikFetchingBookService;
import bookstore.scraper.fetcher.merlin.MerlinFetchingBookService;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import bookstore.scraper.urlproperties.MerlinUrlProperties;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class BestSellersServiceTest {

    @Mock
    private EmpikFetchingBookService empikBookService;
    @Mock
    private MerlinFetchingBookService merlinBookService;
    @Mock
    private EmpikUrlProperties empikUrlProperties;
    @Mock
    private MerlinUrlProperties merlinUrlProperties;

    @InjectMocks
    private BestSellersService bestSellersService;

    @Test
    public void getBestSellers() {
        List<Book> merlinBestsellers = EmpikBookProvider.prepare5Bestsellers();
        List<Book> empikBestsellers = MerlinBookProvider.prepare5Bestsellers();

        when(empikBookService.get5BestSellersEmpik(any())).thenReturn(empikBestsellers);
        when(merlinBookService.get5BestSellersMerlin(any())).thenReturn(merlinBestsellers);
        //when(empikUrlProperties.getEmpik().getBestSellers()).thenReturn(anyString());
        //when(merlinUrlProperties.getMerlin().getBestSellers()).thenReturn(anyString());

        Map<Bookstore, List<Book>> actualMap = bestSellersService.getBestSellers();
        Map<Bookstore, List<Book>> expectedMap = null;

        assertEquals(expectedMap, actualMap);
        assertThat(actualMap).hasSize(expectedMap.size());
    }
}

Without setting behaviour for properties classes as I thought it is unnecessary, because I put any() when calling empikBookService.get5BestSellersEmpik (same for merlinBookService) but it threw NPE when calling

bookstoreWithBestSellers.put(Bookstore.EMPIK, empikBookService
                .get5BestSellersEmpik(connect(empikUrlProperties.getEmpik().getBestSellers())));

I've debugged it and I have seen that

empikUrlProperties.getEmpik().getBestSellers()))

was given me NPE. So I set behaviour like this:

when(empikUrlProperties.getEmpik().getBestSellers()).thenReturn(anyString());
when(merlinUrlProperties.getMerlin().getBestSellers()).thenReturn(anyString());

and now it is giving me here NPE with stactrace:

ava.lang.NullPointerException
    at bookstore.scraper.book.scrapingtypeservice.BestSellersServiceTest.getBestSellers(BestSellersServiceTest.java:48)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
.
.
.

connect method used in tested method:

@UtilityClass
public class JSoupConnector {

    public static Document connect(String url) {
        try {
            return Jsoup.connect(url).get();
        } catch (IOException e) {
            throw new IllegalArgumentException("Cannot connect to" + url);
        }
    }
}

Properties class (same for merlin)

package bookstore.scraper.urlproperties;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
@ConfigurationProperties("external.library.url")
public class EmpikUrlProperties {

    private Empik empik = new Empik();

    @Getter
    @Setter
    public static class Empik {

        private String mostPreciseBook;
        private String bestSellers;
        private String concreteBook;
        private String romances;
        private String biographies;
        private String crime;
        private String guides;
        private String fantasy;
    }
}

What am I making wrong? Why it didnt work in the first place when I put any()

Upvotes: 0

Views: 1015

Answers (2)

racraman
racraman

Reputation: 5034

When you set your mock :

when(empikUrlProperties.getEmpik().getBestSellers()).thenReturn(anyString());

You have mocked empikUrlProperties, which is great, BUT you have not told that mock what to do when getEmpik() is called on it. Consequently, that method call will return null both here in the test, and also (before you had this line) in the production code - so this is the cause of your NPE when getBestSellers() is called.

Consequently, set that mock up, something like :

@Mock
private EmpikUrlProperties empikUrlProperties;
@Mock
private EmpikUrlProperties.Empik empikMock;

when(empikUrlProperties.getEmpik()).thenReturn(empikMock);
when(empikMock.getBestSellers()).thenReturn(anyString());

Upvotes: 1

JB Nizet
JB Nizet

Reputation: 692003

There are many problems, but the main one is that you're misunderstanding how mocking, argument evaluation and any() work.

You're using

when(empikBookService.get5BestSellersEmpik(any())).thenReturn(empikBestsellers);

This tells the mock empikBookService than whenever its get5BestSellersEmpik method is called, it should return empikBestsellers, whetever the argument passed to the method is.

What does your actual code pass as argument when executing your test? It passes the value returned by

connect(empikUrlProperties.getEmpik().getBestSellers())

The key part is that this expression is evaluated first, and then its result is passed as argument to the get5BestSellersEmpik() method.

Just like when you do

System.out.println(a + b)

a + b is first evaluated. If the result is 42, then the value 42 is passed to println(), and println prints 42.

So, in order for your test not to fail, the expression

 connect(empikUrlProperties.getEmpik().getBestSellers())

must be evaluated successfully. Its result doesn't matter, since you've configure your mock to accept any argument. But that's irrelevant.

You're trying to do

when(empikUrlProperties.getEmpik().getBestSellers()).thenReturn(anyString());

That doesn't make any sense.

First, because empikUrlProperties.getEmpik() will return null, since empikUrlProperties is a mock, and mocks return null by default. And null.getBestSellers() will thus cause a NullPointerException.

Second, because telling a mock that it should return any string doesn't make sense. If you don't care about the string it should return, then choose a string by yourself, and make it return that. anyString() actually returns null, so you're telling it to return null.

So you need to fix that. Always think about what your code is doing instead of trying to apply a recipe.

And finally, your test also calls connect(...), which is a staic method, that you haven't (and can't) mock. This method will be called too. And it tries to connect to an actual URL. So if nothing is responding during your test at that URL, that won't work either. This connect() method should really be part of a dependency of your service, and this dependency should be mocked.

Upvotes: 1

Related Questions