如何避免在 Java 中测试静态方法?

How to avoid testing static method in Java?

我想测试连接到 URL 的 class 以解析 html 文件(我正在使用 Jsoup)。问题是我不知道如何测试这个。我知道 PowerMockito 允许这样做,但如果可能的话,我宁愿通过重构代码并只测试重要部分来避免它。

这是我要进行单元测试的代码片段:

@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();
}

}

问题是我不知道如何解耦我的方法以便能够在不必调用 Jsoup.connect(url).get

的情况下进行测试

我的方法是“注入”一些returns核心对象:

而不是做:

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

你可以有一个静态字段:

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

还有两个构造函数:

BaseHtmlToHtml(Function<String, Document> documentReader) {
  this.documentReader = documentReader;
}

BaseHtmlToHtml() {
  this(Jsoup::connect);
}

protected Document getDocument(String url) throws IOException {
    return documentReader.apply(url);
}

然后在你的测试中使用第一个构造函数,或者添加一个setter并更改默认值。

您也可以为此创建一个特定的 bean 并将其注入:在这种情况下,您只需要一个构造函数并确保注入 Jsoup::connect

这是一种无需模拟静态方法的方法 - 但您仍然需要模拟其余方法(例如:读取 url 并将其转换为 Document)。


根据评论,这是一个带有 Spring bean 的示例:

声明一个完成工作的 bean:

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

并且在您的生产代码中,声明一个使用 Jsoup 的 bean:

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

让您的消费者使用这个 bean:

private final DocumentResolver resolver;

BaseHtmlToHtml(DocumentResolver resolver) {
  this.resolver = resolver;
}

protected Document getDocument(String url) throws IOException {
    return resolver.resolve(url);
}

在您的测试中,当您需要模拟行为时:

在您的测试中不使用 Spring 注入:在您的 JUnit 5 + AssertJ 测试中:

@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!")
  ;
}

当然,你必须修复任何你需要修复的东西(例如:类型)。

这个例子没有使用Spring注入:如果你想模拟DocumentResolver,我认为你不能求助于注入,或者如果你这样做,你将不得不重新设置每次模拟,除非 Spring 测试为每次测试执行生成一个新容器:

@TestConfiguration
static class MyTestConfiguration {

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

然后使用 JUnit 5 参数解析器:

@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!")
  ;
}

请注意,我对此一无所知,您将不得不尝试。

此文档可能:帮助 https://www.baeldung.com/spring-boot-testing