0

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

1 Answer 1

4

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

Sign up to request clarification or add additional context in comments.

6 Comments

A couple of suggestions to the good solution above: 1) Instead of having a constructor just for test, keep only the constructor which takes the Function and make the Function final. Then, make a Bean for the function Jsoup::connect in production and inject whatever you'd like to mock in test. 2) The original getDocument throws an IOException that can't be thrown in a Function. Either you create your own functional interface ThrowingFunction and use that instead, or you will need to wrap the IOException into a runtime exception (but I'd go for the first option)
I totally don't know what Jsoup::connect returns :) I think the OP will still get the gist of my idea here.
Sure, me neither, I just see one single line of code in his method and a beautiful "throws IOException" so I just guessed :) But I enforce your solution, it's the way I would have gone and that's why I upvoted :)
Thanks a lot for your answer. However, I do not understand what you meant by "fix the return type (Document)"
Oh simply that Jsoup.connect(String) does not return a Document, but a Connection. So, Jsoup::connect signature is Function<String, Connection> (I just checked the API jsoup.org/apidocs/org/jsoup/… )
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.