Docker を使用した保守可能な統合テストの作成

Testcontainer は、多くの言語での統合テストを容易にすることに焦点を当てたオープンソースコミュニティです。 ジャンルカ・アルベッツァーノは、Influx Data の SRE である Docker キャプテンであり、Docker API を使用してテスト ケースで使用できるテストに適したライブラリを公開する Testcontainer の Golang 実装のメンテナです。 

Markus spiske code unsplash
アンスプラッシュのマーカス・スピスケによる写真。
マイクロサービスの人気と、ビジネスクリティカルでない機能に対するサードパーティサービスの使用により、最新のアプリケーションを構成する統合の数が大幅に増加しました。 最近では、MySQL、Redisをキーバリューストア、MongoDB、Postgress、InfluxDBとして使用するのが一般的ですが、これはすべてデータベース専用であり、アプリケーションの他の部分を構成する複数のサービスは言うまでもありません。

これらの統合ポイントはすべて、異なるテスト層を必要とします。 単体テストでは、すべての依存関係をモックし、関数の期待値を設定し、目的の変換が得られるまで反復できるため、コードの記述速度が向上します。 しかし、もっと必要です。 Redis、MongoDB、またはマイクロサービスとの統合が、モックが記述どおりに機能するだけでなく、期待どおりに機能することを確認する必要があります。 どちらも重要ですが、その違いは非常に大きいです。

この記事では、testcontainer を使用して、非常に低いオーバーヘッドで Go で統合テストを記述する方法を紹介します。 ですから、明確にするために、単体テストの作成をやめるように言っているのではありません。

当時、Java開発者になることに興味があったとき、人気のあるオープンソーストレーサーであるZipkinとInfluxDBの統合を書こうとしました。 私はJava開発者ではないため、最終的には失敗しましたが、彼らが統合テストをどのように書いたかを理解し、魅了されました。

はじめに:テストコンテナ-java

Zipkinは、トレースを保存および操作するためのUIとAPIを提供し、Cassandra、インメモリ、ElasticSearch、MySQL、およびストレージとしてさらに多くのプラットフォームをサポートします。 すべてのストレージシステムが機能することを検証するために、「テストフレンドリー」になるように設計されたdocker-apiのラッパーである testcontainers-java と呼ばれるライブラリを使用します。ここは クイックスタート の例です。

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環境をクリーンに保ちます。 テスト内からそれらを削除することもできます。以下の例は ジプキンからのものです。 彼らは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開発者ではないので、ライブラリをGolangに移植しました(まだすべての機能に取り組んでいます)が、現在はメインのテスト コンテナ/ testcontainers-go 組織にあります。

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) をクリーンアップします。 リュークを覚えていますか?これは必須のコマンドではありませんが、testcontainers-goはこれを使用して、ある時点でコンテナを削除します。

モジュール

Javaライブラリには、データベース(mysql、postgress、cassandraなど)やnginxなどのアプリケーションなどの事前に用意されたコンテナを取得する モジュール と呼ばれる機能があります。goバージョンは同様のものに取り組んでいますが、それでも オープンPRです。

アプリケーションが依存するマイクロサービスをアップストリーム ビデオから構築する場合、これは優れた機能です。 または、アプリケーションがコンテナ内からどのように動作するかをテストしたい場合(おそらく、prodで実行される場所に似ています)。 これは 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クラスターを起動する新しい canned コンテナです。 アプリケーションで Kubernetes API を使用している場合は、統合でテストできます。

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からわかるように、まだ進行中の作業です。

すべてのコーダーを呼び出す

テストコンテナ用のJavaバージョンは最初に開発されたものであり、GoバージョンやJavaScript、Rust、.Netなどの他のライブラリに移植されていない多くの機能を備えています。

私の提案は、あなたの言語で書かれたものを試して、それに貢献することです。

Goでは、プログラムでイメージをビルドする方法はありません。 Dockerに依存しないデーモンレスビルダーを取得するために、埋め込み buildkit または img することを考えています。 Goバージョンで作業することの大きな部分は、すべてのコンテナ関連ライブラリがすでにGoにあるため、それらとの統合の非常に優れた作業を行うことです。

これはこのコミュニティの一員になる絶好のチャンスです! フレームワークのテストに情熱を注いでいる場合は、私たちに参加してプルリクエストを送信するか、 Slackにたむろしてください。

試してみよう

このライブラリが提供する味と力について、私と同じように興奮していただければ幸いです。 GitHub の testcontainers 組織を見て、あなたの言語がカバーされているかどうかを確認し、試してみてください。 そして、あなたの言語がカバーされていない場合は、それを書きましょう! あなたがGo開発者であり、貢献したい場合は、 @gianarb私に連絡するか、それをチェックして問題またはプルリクエストを開いてください!