fbpx

Dockerでメンテナンス可能な統合テストを書こう! #docker

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

オープンソースコミュニティでは多言語にわたる統合テストを容易にするための注力がなされています。Gianluca Arbezzano氏はDockerキャプテンであり、 Influx Data社のSREです。彼はまた、Docker APIを使用してユーザが自身のテストケース内で使用できるテストに便利なライブラリを公開するtestcontainersの、Go言語版のメンテナでもあります。

マイクロサービスの人気と、ビジネスクリティカルでない機能のためのサードパーティのサービス利用によって、モダンなアプリケーションを構築するためのサービス間の統合が大幅に増加しました。最近では、マルチサービスは言うまでもなく、MySQL、キーバリューストアとしてのRedis、MongoDB、Postgress、InfluxDBなどの利用が一般的となりました。これらはすべてデータベース専用であり、アプリケーションの他の部分を構成しています。

これらすべての統合ポイントで、それぞれのレイヤーでのテストが必要です。単体テストは、すべての依存関係を模倣し、機能の期待値を設定し、あるべき状態を得るまで繰り返せることから、コードを書くスピードを早めることができます。しかしそれだけでは足りません。モックが書いた通りに動作するだけでなく、RedisやMongoDBとの統合やマイクロサービスが、期待通りに確実に動くようにしなければなりません。両方とも重要ですが、両者の違いは多大なるものです。

この記事では、オーバーヘッドがとても少ない統合テストをGo言語で書くための、testcontainersの使い方をご紹介します。単体テストを書くのをやめてください、と言っているわけではありませんのでご安心ください!

その昔、私がJava開発者になることに興味を抱いていた頃、人気のオープンソーストレーサであるZipkinと、InfluxDBの統合テストを書こうとしたことがありました。結局それは失敗に終わりました。なぜなら私はJava開発者ではなかったからです。しかし統合テストがどのように書かれたかについては理解することができました。そしてそれに夢中になりました。

まずはじめに: testcontainers-java

Zipkinは、トレースの格納と操作を行うUIとAPIを提供しており、これはCassandraをはじめ、インメモリやElasticSearch、そしてMySQL、その他多くのプラットフォームをストレージとしてサポートするものです。すべてのストレージシステムが適切に機能することを検証するために、testcontainers-javaというライブラリを使用します。これは、docker-apiのラッパーで、「テストしやすい」よう設計されています。Quick Startの例をご紹介しましょう:

public class RedisBackedCacheIntTestStep0 {
    private RedisBackedCache underTest;

    @Before
    public void setUp() {
        // Assume that we have Redis running locally?
        underTest = new RedisBackedCache("localhost", 6379);
    }

    @Test
    public void testSimplePutAndGet() {
        underTest.put("test", "example");

        String retrieved = underTest.get("test");
        assertEquals("example", retrieved);
    }
}

setUpで、コンテナ(今回はRedis)を作成し、ポートを公開することができます。ここから動作中のredisインスタンスとの通信が可能になります。

新しいコンテナを起動するたびに、ryukという「サイドカー」が現れます。これは一定期間ごとにコンテナやボリューム、そしてネットワークを削除することでDocker環境をきれいに保つ働きをするものです。コンテナ、ボリューム、ネットワークはテスト内でも削除することができます。次に示すのはZipkinからの例です。これはElasticSearchの統合テストで、ご覧のとおりテストケース内で依存関係をプログラムによって設定することが可能です。

public class ElasticsearchStorageRule extends ExternalResource {
 static final Logger LOGGER = LoggerFactory.getLogger(ElasticsearchStorageRule.class);
 static final int ELASTICSEARCH_PORT = 9200; final String image; final String index;
 GenericContainer container;
 Closer closer = Closer.create();

 public ElasticsearchStorageRule(String image, String index) {
   this.image = image;
   this.index = index;
 }
 @Override
 
  protected void before() {
   try {
     LOGGER.info("Starting docker image " + image);
     container =
         new GenericContainer(image)
             .withExposedPorts(ELASTICSEARCH_PORT)
             .waitingFor(new HttpWaitStrategy().forPath("/"));
     container.start();
     if (Boolean.valueOf(System.getenv("ES_DEBUG"))) {
       container.followOutput(new Slf4jLogConsumer(LoggerFactory.getLogger(image)));
     }
     System.out.println("Starting docker image " + image);
   } catch (RuntimeException e) {
     LOGGER.warn("Couldn't start docker image " + image + ": " + e.getMessage(), e);
   }

これがプログラムによって行われる、という点が重要です。なぜならこれにより統合テスト環境を起動する際、 docker-composeのような外部ソフトウェアに頼る必要がなくなるからです。テスト自身の中から起動することで、オーケストレーションとプロビジョニングがより制御可能となり、テストはより安定するのです。テストを実行する前に、コンテナの準備が完了しているかを確認することもできます。

私はJava開発者ではないため、Go言語にライブラリを移植し(すべての機能はまだ開発中です)、現在は主要なtestcontainers/testcontainers-goorganizationに入っています。

func TestNginxLatestReturn(t *testing.T) {
    ctx := context.Background()
    req := testcontainers.ContainerRequest{
        Image:        "nginx",
        ExposedPorts: []string{"80/tcp"},
    }
    nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        t.Error(err)
    }
    defer nginxC.Terminate(ctx)
    ip, err := nginxC.Host(ctx)
    if err != nil {
        t.Error(err)
    }
    port, err := nginxC.MappedPort(ctx, "80")
    if err != nil {
        t.Error(err)
    }
    resp, err := http.Get(fmt.Sprintf("http://%s:%s", ip, port.Port()))
    if resp.StatusCode != http.StatusOK {
        t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
    }
}

テストを作成する

次のように表示されます:

ctx := context.Background()
req := testcontainers.ContainerRequest{
    Image:        "nginx",
    ExposedPorts: []string{"80/tcp"},
}
nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
    ContainerRequest: req,
    Started:          true,
})
if err != nil {
    t.Error(err)
}
defer nginxC.Terminate(ctx)

nginxコンテナを作成し、テストが完了したらdefer nginxC.Terminate(ctx)コマンドでコンテナを削除します。ryukを覚えていますか?これは強制コマンドではなく、testcontainers-goがコンテナを適切なタイミングで削除するために利用しています。

モジュール

Javaライブラリにはmodulesと呼ばれる機能があり、ここではデータベース(mysql、postgress、cassandraなど)またはnginxのようなアプリケーションなど、事前にパッケージングしたコンテナを得ることができます。Go言語版でも類似の機能を開発中で、プルリクエストを送っています

アップストリームの動画からアプリケーションが依存するマイクロサービスを構築したい場合は、この機能が適しています。あるいはコンテナ内(本番環境でのアプリケーションの動作により近いと思われます)からアプリケーションの動作をテストしたい場合にも適しています。Javaではこのように機能します:

@Rule
public GenericContainer dslContainer = new GenericContainer(
    new ImageFromDockerfile()
            .withFileFromString("folder/someFile.txt", "hello")
            .withFileFromClasspath("test.txt", "mappable-resource/test-resource.txt")
            .withFileFromClasspath("Dockerfile", "mappable-dockerfile/Dockerfile"))

現在、私が注力していること

現在、私が注力しているものは、Kubernetesクラスタをコンテナ内で起動するための機能を使用する、新しいパッケージング済みコンテナです。Kubernetesを使用するアプリケーションでは、こちらで統合テストを行えます:

ctx := context.Background()
k := &KubeKindContainer{}
err := k.Start(ctx)
if err != nil {
  t.Fatal(err.Error())
}
defer k.Terminate(ctx)
clientset, err := k.GetClientset()
if err != nil {
  t.Fatal(err.Error())
}
ns, err := clientset.CoreV1().Namespaces().Get("default", metav1.GetOptions{})
if err != nil {
  t.Fatal(err.Error())
}
if ns.GetName() != "default" {
  t.Fatalf("Expected default namespace got %s", ns.GetName())

PR67でご確認いただけるとおり、この機能はまだ開発中です。

すべてのコーダーの方々へ

testcontainersのJava版は初めに開発され、多くの機能を備えています。そのすべてはまだGo言語版には移植されておらず、JavaScript、Rust、.Netなど他のライブラリへも同様です。

あなたの使用言語で書かれているものをお試しください。またコントリビュートも歓迎です。

Go言語では、プログラムによるイメージビルドの方法はありません。Dockerに依存しないデーモンレスなビルダーを得るために、buildkitまたはimgを埋め込むことを考えています。Go言語版で作業する素晴らしい点は、すべてのコンテナ依存のライブラリはすでにGo言語で書かれているため、非常に統合性に優れているということです。

これはコミュニティに参加する素晴らしい機会です!もしあなたがフレームワークのテストに情熱を抱いているなら、ぜひコミュニティに参加し、プルリクエストを送ってください。またはSlackに参加しやり取りをしましょう。

試してみよう

このライブラリが提供する機能のテイストとパワーについて、みなさんが私と同じくらいワクワクしてくれていることを願います。GitHubでtestcontainers organizationで、あなたの使用言語が利用可能か確認し、ぜひお試しください!もしあなたの使用言語が使用可能となっていない場合は、自分で書いてみましょう!もしもあなたがGo開発者でコントリビュートしたい場合は、お気軽に@gianarbへ私宛てにお問合せください。またはissueをオープンし、プルリクエストをお送りください!


原文: Write Maintainable Integration Tests with Docker

New call-to-action
新規CTA