从单元测试中消除空值的方法

时间:2019-08-10 09:42:08

标签: java unit-testing mockito

在尝试连接此URL时,方法中的变量url导致NPE(使用givenTitle解析为null)。

package bookstore.scraper.book.booksource.empik;

import bookstore.scraper.book.Book;
import bookstore.scraper.book.booksource.BookServiceSource;
import bookstore.scraper.enums.Bookstore;
import bookstore.scraper.enums.CategoryType;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import bookstore.scraper.utilities.JSoupConnector;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.IntStream;

@Service
public class EmpikSource implements BookServiceSource {

    private static final int FIRST_PART_PRICE = 0;
    private static final int SECOND_PART_PRICE = 1;

    private static final int BESTSELLERS_NUMBER_TO_FETCH = 5;
    private static final int CATEGORIZED_BOOKS_NUMBER_TO_FETCH = 15;
    private static final String DIV_PRODUCT_WRAPPER = "div.productWrapper";
    private static final String DATA_PRODUCT_ID = "data-product-id";

    private final EmpikUrlProperties empikUrlProperties;
    private final JSoupConnector jSoupConnector;
    private Map<CategoryType, String> categoryToEmpikURL;

    @Autowired
    public EmpikSource(EmpikUrlProperties empikUrlProperties, JSoupConnector jSoupConnector) {
        this.empikUrlProperties = empikUrlProperties;
        this.jSoupConnector = jSoupConnector;
        categoryToEmpikURL = createCategoryToEmpikURLMap();
    }

    @Override
    public Bookstore getName() {
        return Bookstore.EMPIK;
    }


    @Override
    public Book getMostPreciseBook(String givenTitle) {
        String concatedUrl = concatUrlWithTitle(categoryToEmpikURL.get(CategoryType.MOST_PRECISE_BOOK), givenTitle);

        Document document = jSoupConnector.connect(concatedUrl);

        String author = document.select("div.smartAuthorWrapper.ta-product-smartauthor").select("a").first().text();
        String price = convertEmpikPriceWithPossibleDiscountToActualPrice(document.select("div.price.ta-price-tile").first().text());
        String title = document.select(DIV_PRODUCT_WRAPPER).select("strong").first().text();
        String productID = document.select(DIV_PRODUCT_WRAPPER).select("a").first().attr(DATA_PRODUCT_ID);
        String bookUrl = createBookURL(title, productID);

        return Book.builder()
                .author(author)
                .price(price)
                .title(title)
                .productID(productID)
                .bookURL(bookUrl).build();
    }

    private String concatUrlWithTitle(String url, String title) {
        return String.format(url, title);
    }
private Map<CategoryType, String> createCategoryToEmpikURLMap() {
        Map<CategoryType, String> map = new EnumMap<>(CategoryType.class);

        map.put(CategoryType.CRIME, empikUrlProperties.getCrime());
        map.put(CategoryType.BESTSELLER, empikUrlProperties.getBestSellers());
        map.put(CategoryType.BIOGRAPHY, empikUrlProperties.getBiographies());
        map.put(CategoryType.FANTASY, empikUrlProperties.getFantasy());
        map.put(CategoryType.GUIDES, empikUrlProperties.getGuides());
        map.put(CategoryType.MOST_PRECISE_BOOK, empikUrlProperties.getMostPreciseBook());
        map.put(CategoryType.ROMANCES, empikUrlProperties.getRomances());

        return map;
    }
}

测试:

package bookstore.scraper.book.booksource.empik;

import bookstore.scraper.book.Book;
import bookstore.scraper.enums.CategoryType;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import bookstore.scraper.utilities.JSoupConnector;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;

import static bookstore.scraper.dataprovider.EmpikBookProvider.prepare15CrimeBooks;
import static bookstore.scraper.dataprovider.EmpikBookProvider.prepareMostPreciseBook;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class EmpikSourceTest {

    @Mock
    JSoupConnector jSoupConnector;
    @Mock
    EmpikUrlProperties empikUrlProperties;

    @InjectMocks
    EmpikSource empikSource;

    /*@Before
    public void setUp() {
        when(empikUrlProperties.getConcreteBook()).thenReturn(anyString());
    }
*/

    @Test
    public void getMostPreciseBook() throws IOException {
        File in = getFile("/empik/MostPreciseBookEmpik.html");
        Document empikDocument = Jsoup.parse(in, "UTF-8");

        when(jSoupConnector.connect(any())).thenReturn(empikDocument);
        when(empikUrlProperties.getConcreteBook()).thenReturn("https://www.empik.com/%s,%s,ksiazka-p");

        Book actualBooks = empikSource.getMostPreciseBook("W pustyni i w puszczy. Lektura z opracowaniem - Henryk Sienkiewicz");
        Book expectedBooks = prepareMostPreciseBook();

        assertEquals(expectedBooks, actualBooks);
    }

    private File getFile(String resourceName) {
        try {
            return new File(EmpikSourceTest.class.getResource(resourceName).toURI());
        } catch (URISyntaxException e) {
            throw new IllegalStateException(e);
        }
    }

}

我不应该将url作为null吗?

Stacktrace:

java.lang.NullPointerException
    at java.base/java.util.regex.Matcher.getTextLength(Matcher.java:1769)
    at java.base/java.util.regex.Matcher.reset(Matcher.java:416)
    at java.base/java.util.regex.Matcher.<init>(Matcher.java:253)
    at java.base/java.util.regex.Pattern.matcher(Pattern.java:1130)
    at java.base/java.util.Formatter.parse(Formatter.java:2698)
    at java.base/java.util.Formatter.format(Formatter.java:2653)
    at java.base/java.util.Formatter.format(Formatter.java:2607)
    at java.base/java.lang.String.format(String.java:2734)
    at bookstore.scraper.book.booksource.empik.EmpikSource.concatUrlWithTitle(EmpikSource.java:154)
    at bookstore.scraper.book.booksource.empik.EmpikSource.getMostPreciseBook(EmpikSource.java:74)
    at bookstore.scraper.book.booksource.empik.EmpikSourceTest.getMostPreciseBook(EmpikSourceTest.java:66)

EmpikUrl属性:

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.empik")
public class EmpikUrlProperties {

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

1 个答案:

答案 0 :(得分:2)

选项1:

getCategory上添加新方法empikUrlProperties

public String getCategory(CategoryType category) {
    assert (category != null);

    switch (category) {
        case CategoryType.CRIME:
            return getCrime();
        case CategoryType.BESTSELLER:
            return getBestSellers();
        case CategoryType.BIOGRAPHY:
            return getBiographies();
        case CategoryType.FANTASY:
            return getFantasy();
        case CategoryType.GUIDES:
            return getGuides();
        case CategoryType.MOST_PRECISE_BOOK:
            return getMostPreciseBook();
        case CategoryType.ROMANCES:
            return getRomances();
        default:
            throw new IllegalArgumentException("Unexpected category: " + category)
    }
}

现在只需简单地模拟测试

when(empikUrlProperties.getCategory(CategoryType.CRIME))
    .thenReturn("https://www.empik.com/%s,%s,ksiazka-p");

选项2:

更改构造函数以从外部接受地图。 这意味着您将地图创建移至另一个类/函数, 会在其他位置创建地图。

在这种情况下,您需要手动注入模拟(不要使用@InjectMocks), 因为您不应该嘲笑集合。

(对于这一部分,我不确定@Autowired的处理方式,也许您需要将其包装到另一个类中。设置器可能是一个更合适的选择。)

@Autowired
public EmpikSource(EmpikUrlProperties empikUrlProperties, JSoupConnector jSoupConnector, Map<CategoryType, String> categoryToEmpikURL ) {
    this.empikUrlProperties = empikUrlProperties;
    this.jSoupConnector = jSoupConnector;
    categoryToEmpikURL = categoryToEmpikURL;
}