LaChope
LaChope

Reputation: 313

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

Upvotes: 0

Views: 625

Answers (1)

NoDataFound
NoDataFound

Reputation: 11949

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

Upvotes: 4

Related Questions