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
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