使用embedded-elasticsearch轻松进行Elasticsearch的JUnit测试 2018年版

内嵌的Elasticsearch:在测试代码中启动ES

大家对于使用Elasticsearch进行单元测试和集成测试有什么方法呢?
我认为有很多种方法,比如创建一个模拟ES客户端并注入;连接到外部运行的ES实例;等等。但是,在开发Java应用程序时有一种很方便的方法,就是使用嵌入式Elasticsearch。
也就是说,由于ES本身就是一个Java应用程序,所以我们可以在执行测试的JVM上同时启动ES节点,并将其用作测试用的ES实例。这个实例是真实的ES实例,而不是模拟的,所以索引和搜索功能都正常工作,非常方便。此外,elastic本身也提供了一个名为ESIntegTestCase的JUnit测试模板,我几年前也写过一篇文章。

然而,自从版本5以后,这个嵌入式的Elasticsearch变得越来越难以使用。

    • elastic自身がEmbedded Elasticsearchはサポート外と宣言

 

    • 公式PluginがMaven Centralにアップロードされなくなった

 

    Elasticsearch本体もモジュール化が進んだが、重要なモジュールがやはりアップロードされなくなった

由于在公司内部使用了基于Embedded Elasticsearch的测试框架并深度利用,所以在从ES2.x升级时,我必须和同事一起抱怨,并逐个将测试所需的模块注册到内部的Maven仓库,这是一项繁琐的工作。

似乎还有其他人遇到了类似的问题,当我在 Elastic 的论坛上进行调查时,找到了以下的替代方案建议。

Elasticsearch Maven Pluginを使う
Docker等でテスト用のESインスタンスを実行する
Gradleを用いる方法もあるらしい

简而言之,这是一种通过与构建工具协作来运行测试用ES实例的方法。虽然这种方法是可行的,但它失去了在IDE上直接执行测试用例并能够从测试代码中直接控制ES节点的设置和启动,以及使用嵌入式Elasticsearch的许多优点,比如方便性和可编程性。

因此,一些库已经开发出来,可以在ES5.x及更高版本上实现类似于嵌入式Elasticsearch的功能。

    • codelibs/elasticsearch-cluster-runner

 

    allegro/embedded-elasticsearch

elasticsearch-cluster-runner是一个用于在ES5.x及更高版本中使用嵌入式Elasticsearch的库。没有上传到Maven Central的ES模块是由我们自己上传到了Maven Central。我们非常感谢您。

embedded-elasticsearch是一种稍微不同的方法。尽管名为embedded-elasticsearch,但此库并不在与测试相同的JVM上运行ES。相反,它会按照以下步骤将ES实例作为外部进程在每次测试中执行。

    • 公式リポジトリからElasticsearchの配布パッケージをダウンロードする

 

    • ダウンロードしたパッケージを展開する

 

    • 設定ファイルを作成する

 

    • プラグインをインストールする

bin/elasticsearchを実行してESインスタンスを起動する
テスト終了後ESインスタンスのプロセスを停止する

换句话说,您可以通过常规方式下载和运行ES,就像获取Embedded Elasticsearch一样,同时还可以通过测试代码来可编程地控制ES节点,这样方便易用。

每个都有其优点和缺点。

elasticsearch-cluster-runner: 起動が早い・より高機能・デフォルトで複数ノード立ち上げ

embedded-elasticsearch: 依存性がとても少ない

由于Embedded Elasticsearch需要在同一个JVM上启动ES,所以运行ES所需的所有依赖项都会添加到类路径中。因此,它可能会与本来想要测试的应用程序的依赖性发生冲突。另一方面,由于embedded-elasticsearch将ES作为外部进程运行,所以不会出现类似的问题。embedded-elasticsearch本身只依赖于Jackson和一些常见的commons库,非常轻量。

虽然前言过长,但本篇文章将尝试在JUnit上使用嵌入式Elasticsearch。

嵌入式Elasticsearch的基本使用方法。

依赖性的设定

请将 pl.allegro.tech:embedded-elasticsearch 添加到依赖项中。最新版本是2.5.0。由于最近在Windows上修复了问题,建议使用最新版本。

  <dependencies>
    ...
    <dependency>
      <groupId>pl.allegro.tech</groupId>
      <artifactId>embedded-elasticsearch</artifactId>
      <version>2.5.0</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.21</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.1.0</version>
      <scope>test</scope>
    </dependency>

  </dependencies>

embedded-elasticsearch在日志记录中使用SLF4J,但因为只包含slf4j-api作为依赖,所以如果没有适当的binding依赖,也会被添加(在上面的例子中是slf4j-simple)。
在本例中,我们使用JUnit5作为测试框架,但当然也可以无问题地使用JUnit4或ScalaTest。

测试代码

测试代码将呈现出以下形式。

public class TestEmbeddedES {

  private static EmbeddedElastic es = null;

  @BeforeAll
  public static void startES() throws Exception {

    es = EmbeddedElastic.builder()
      .withElasticVersion("6.2.4")
      .withSetting("discovery.zen.ping.unicast.hosts", Collections.emptyList()) // <- (*)
      .build();

    es.start();
  }

  @Test
  void check_es_status() throws IOException {

    System.out.println("HTTP port: " + es.getHttpPort());
    System.out.println("Transport port: " + es.getTransportTcpPort());

    CloseableHttpClient client = HttpClientBuilder.create().build();

    String uri = "http://localhost:" + es.getHttpPort() + "/";
    CloseableHttpResponse res = client.execute(new HttpGet(uri));
    System.out.println(IOUtils.toString(res.getEntity().getContent(), "UTF-8"));
  }

  @AfterAll
  public static void stopES() {
    es.stop();
  }

}

在执行所有测试之前,使用EmbeddedElastic的实例构建器来创建它。可以通过构建器来指定ES的版本和配置(最终会写入config/elasticsearch.yml)。(*)所示的配置是禁用ES的Zen发现的一种方法。如果在同一台机器上运行ES,则没有这个配置的话,以测试目的启动的ES节点可能会错误地连接到正在运行的ES上。

调用EmbeddedElastic#start()会启动ES。可以通过调用getHttpPort()或getTransportTcpPort()获取连接已启动的ES所使用的端口号。在实际测试中,您可能需要使用这些端口号来创建客户端、设置测试服务,并进行测试。

在所有测试结束后(@AfterAll),请确保调用EmbeddedElastic#stop()来停止Elasticsearch。

基本操作

执行上述测试会生成以下日志。

[main] INFO p.a.t.e.ElasticSearchInstaller - Downloading https://artifacts.elastic.co/.../elasticsearch-6.2.4.zip to /var/folders/pl/l3n21...
[main] INFO p.a.t.e.ElasticSearchInstaller - Download complete
[main] INFO p.a.t.e.ElasticSearchInstaller - Installing Elasticsearch into /var/folders/pl/l3n21...
[main] INFO p.a.t.e.ElasticSearchInstaller - Done
[main] INFO p.a.t.e.ElasticSearchInstaller - Applying executable permissions on /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch-plugin
[main] INFO p.a.t.e.ElasticSearchInstaller - Applying executable permissions on /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch
[main] INFO p.a.t.e.ElasticSearchInstaller - Applying executable permissions on /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch.in.sh
[main] INFO p.a.t.e.ElasticServer - Waiting for ElasticSearch to start...
[EmbeddedElsHandler] INFO p.a.t.e.ElasticServer - [2018-04-20T00:40:05,110][INFO ][o.e.n.Node               ] [] initializing ...
...

正如日志中所显示的,embedded-elasticsearch首先从elastic服务器下载指定版本的ES分发包。下载完成后,它会将其解压并设置文件的执行属性,然后执行ES。下载的ES分发包将被缓存到临时目录中。如果临时目录没有被清理,后续的下载将被跳过。

[main] INFO p.a.t.e.ElasticSearchInstaller - Download skipped
[main] INFO p.a.t.e.ElasticSearchInstaller - Installing Elasticsearch into /var/folders/pl/l3n21...
[main] INFO p.a.t.e.ElasticSearchInstaller - Done
...

然而,在初始设置中,每次执行测试类时都需要解压ES分发包,因此比elasticsearch-cluster-runner慢。

使用插件

在与ES实例一起使用的情况下,可以使用EmbeddedElastic.Builder的withPlugin(…)方法来指定要使用的插件。

  ...
  @BeforeAll
  public static void startES() throws Exception {

    es = EmbeddedElastic.builder()
      .withElasticVersion("6.2.4")
      .withPlugin("analysis-phonetic")
      .withPlugin("analysis-icu")
      .withSetting("discovery.zen.ping.unicast.hosts", Collections.emptyList())
      .build();
    ...

如果指定了插件,则在ES启动之前会使用bin/elasticserch-plugin命令安装插件。

...
[main] INFO p.a.t.e.ElasticSearchInstaller - > /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch-plugin install analysis-phonetic
-> Downloading analysis-phonetic from elastic
[=================================================] 100%   
-> Installed analysis-phonetic
[main] INFO p.a.t.e.ElasticSearchInstaller - > /var/folders/pl/l3n21.../elasticsearch-6.2.4/bin/elasticsearch-plugin install analysis-icu
-> Downloading analysis-icu from elastic
[=================================================] 100%   
-> Installed analysis-icu
...

在这里的问题是,ES的分发包一旦下载就会被缓存,而插件则需要每次测试都重新下载。对于像音素分析插件这样小的插件可能还可以接受,但对于像国际货币基金组织插件这样的大型插件来说,每次都要下载进行测试是非常痛苦的。

遗憾的是,目前嵌入式 Elasticsearch 库本身并没有针对这个问题的特定解决方案。然而,由于 withPlugin(…) 方法在内部实际上执行 bin/elasticserch-plugin,因此 withPlugin(…) 方法的参数可以指定插件名称、URL 或本地文件路径。

因此,如果希望在测试中使用插件,实际上需要自行实现一个机制,在启动 EmbeddedElastic 之前下载并将插件包缓存到本地。

所以,应该使用哪个库好呢?

根据应用程序使用的ES客户端类型来决定。
Transport客户端目前依赖于elasticsearch-core等多个ES模块。因此,在添加了transport模块的依赖时,与使用elasticsearch-cluster-runner的情况下,类路径内容几乎没有太大差别。
因此,在这种情况下,我认为启动快且功能强大的elasticsearch-cluster-runner是一个不错的选择。

如果您使用的是对依赖性要求较少的REST客户端,并且不想增加太多依赖性,embedded-elasticsearch也是一个不错的选择。

我重新调查了elasticsearch-cluster-runner,发现它非常强大,而且重新确认了Elasticsearch本身并不依赖太多,所以我改变了观点,认为使用embedded-elasticsearch的场景可能比我想象的要少。虽然这次是关于我熟悉的embedded-elasticsearch的文章,但我也打算写一篇关于使用elasticsearch-cluster-runner的文章。

bannerAds