Search code examples
javaunit-testingjunitjsoup

How to avoid testing static method in Java?


I want to test a class that connects to an URL to parse html files (I am using Jsoup). The issue is that I do not know how to test this. I know that PowerMockito allows to do so, but I would prefer to avoid it if possible, by refactoring the code and test only important parts.

Here is the pieces of code I want to unit test:

@Service
public class EurLexHtmlToHtmlService extends BaseHtmlToHtml {

private static final String eurlex_URL = "https://eur-lex.europa.eu/";

@Override
public InputStream urlToHtml(String url, boolean hasOnlyOneSheet, boolean hasBorders) throws IOException {
    Document document = getDocument(url);
    Element content = document.body();

    Element cssLink = document.select("link").last();
    String cssHref = cssLink.attr("href").replace("./../../../../", "");
    //Method of BaseHtmlToHtml
    addStyle(url, content, cssHref);
    // Method of BaseHtmlToHtml
    return toInputStream(content);
    }
}

public abstract class BaseHtmlToHtml implements HtmlToHtmlService {

@Autowired
HtmlLayout htmlLayout;

protected ByteArrayInputStream toInputStream(Element content) throws IOException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    outputStream.write(content.outerHtml().getBytes());
    outputStream.close();
    return new ByteArrayInputStream(outputStream.toByteArray());
}

protected void addStyle(String url, Element content, String cssHref) throws IOException {
    Document cssDoc = getDocument(url + cssHref);
    Elements cssElements = cssDoc.getAllElements();
    content.append(htmlLayout.getOpenStyleTag() + cssElements.outerHtml() + htmlLayout.getCloseStyleTag());
}

protected Document getDocument(String url) throws IOException {
    return Jsoup.connect(url).get();
}

}

The issue is that I do not know how to decouple my methods to be able to test without having to call Jsoup.connect(url).get


Solution

  • The way I do it is by "injecting" something that returns the core object:

    Instead of doing:

    protected Document getDocument(String url) throws IOException {
        return Jsoup.connect(url).get();
    }
    

    You could have a static field:

    private final Function<String, Document> documentReader; // fix the return type (Document)
    

    And two constructor:

    BaseHtmlToHtml(Function<String, Document> documentReader) {
      this.documentReader = documentReader;
    }
    
    BaseHtmlToHtml() {
      this(Jsoup::connect);
    }
    
    protected Document getDocument(String url) throws IOException {
        return documentReader.apply(url);
    }
    

    Then use the first constructor in your test, or add a setter and change the default value.

    You could also create a specific bean for that and inject it instead: in such case, you need only one constructor and ensure that you inject the Jsoup::connect instead.

    That's one way to do it without mocking a static method - but you will still have to mock the rest (eg: reading the url and converting it to a Document).


    Per the comment, here is a sample with a Spring bean:

    Declare a bean that does the work:

    @FunctionalInterface
    interface DocumentResolver {
      Document resolve(String url) throws IOException;
    }
    

    And in your production code, declare a bean that use Jsoup:

    @Bean 
    public DocumentResolver documentResolver() {
      return url -> Jsoup.connect(url).get();
    }
    

    Have your consumer use this bean:

    private final DocumentResolver resolver;
    
    BaseHtmlToHtml(DocumentResolver resolver) {
      this.resolver = resolver;
    }
    
    protected Document getDocument(String url) throws IOException {
        return resolver.resolve(url);
    }
    

    In your test, when you need to mock the behavior:

    Without using Spring injection in your test: in your JUnit 5 + AssertJ test:

    @Test
    void get_the_doc() {
      DocumentResolver throwingResolver = url -> {
        throw new IOException("fail!");
      };
      BaseHtmlToHtml html = new BaseHtmlToHtml(throwingResolver);
      
      assertThatIOException()
         .isThrownBy(() -> html.urlToHtml("foobar", false, false))
         .withMessage("fail!")
      ;
    }
    

    Of course, you would have to fix whatever you need to fix (eg: the type).

    This example does not use Spring injection: if you want to mock DocumentResolver, I don't think you can resort to injection, or if you do, you will have to reset the mock each time unless Spring Test produce a a fresh container for each test execution:

    @TestConfiguration
    static class MyTestConfiguration {
    
        @Bean 
        public DocumentResolver documentResolver() {
          return mock(DocumentResolver.class);
        }
    }
    

    Then using JUnit 5 parameter resolver:

    @Test
    void get_the_doc(DocumentResolver resolver, BaseHtmlToHtml html) {
      doThrow(new IOException("fail!")).when(resolver).resolve(anyString());
      assertThatIOException()
         .isThrownBy(() -> html.urlToHtml("foobar", false, false))
         .withMessage("fail!")
      ;
    }
    

    Do note I am not knowledgeable on that, you will have to try.

    This doc may: help https://www.baeldung.com/spring-boot-testing