📦

VSCode Dev Containers上でTestcontainersを使う方法

2024/02/14に公開

こんにちは。Nstockのエンジニアの ryan5500 です。

今回は私たちの開発環境であるVSCode Dev Containers上で、Testcontainers (Java版) を利用する方法をご紹介します。

忙しい方向けの要点

Testcontainers (Java版)をVSCode Dev Containers上から利用する場合は、docker compose設定ファイルに以下の設定を追加する

  • Testcontainersがコンテナ起動の指示を出すため
    • volumes/var/run/docker.sock:/var/run/docker.sock を追加する
  • テスト時に開発用コンテナ内からTestcontainersが起動したコンテナに接続するため
    • extra_hostshost.docker.internal:host-gatewayを追加する
    • environmentTESTCONTAINERS_HOST_OVERRIDE=host.docker.internalを追加する

サンプルリポジトリ: https://github.com/ryan5500/spring-boot-and-testcontainers-on-devcontainer-example

前提: Nstockの開発環境

Nstockでは、M1 Mac上でVSCodeのDev Containersを利用して、開発に必要なツールを含めたコンテナで開発しています。具体的には、docker-compose.ymlを用いて、開発用のコンテナとデータベースコンテナを立ち上げるようにしています。

ウェブサービスの開発には、以下の技術スタックを利用しています。

  • フロントエンド: TypeScript + Next.js
  • バックエンド: Java 17 + Spring Boot 2系
    • テスト関連ライブラリ: JUnit5系 + DBRider
  • バッチや運用ツール: Golang
  • データベース: PostgreSQL

チーム体制としては、開発の初期から3名のエンジニアがおり、それぞれ専門性が異なっていました。フロー効率を上げるため、各自が専門の特定領域のみ開発するよりは、フルスタックにやっていきたいと考えました。

フルスタックに開発していくにあたり、以下のようなニーズが生まれたため、現時点では共通の開発環境を利用しています。

  • 環境構築のコストを減らし開発に集中したい
  • 環境起因の問題のデバッグ時間を減らしたい
  • 各言語用の拡張機能を、専門性のある方に設定してもらいたい

バックエンドの統合テストにおけるデータベースアクセスの検討

バックエンドの統合テストを書く場合に、テストにおけるデータベースへのアクセスをどうするかを考える必要があり、以下のような選択肢がありそうでした。

  • テスト時のみin-memoryデータベースを利用する
  • VSCode Dev Containersで起動しているデータベースコンテナ内にテスト用のデータベースを用意し、テスト時にはそのデータベースを利用する
  • テスト用に別途データベースコンテナを用意する

PostgreSQLにおけるRoleごとのポリシー設定など、データベースの種類に依存した安全策を講じている背景から、できるだけ本番に近い環境を用意したいと考えました。

調査を進める中で、以下のメリットから、テスト用に様々なコンテナを起動できるTestcontainersを採用しました。

  • 本番と同じバージョンを指定してデータベースを利用できる
  • テストケースごとにデータベースコンテナを用意できるので、テストを並列実行する際にデータベースの取り合いにならず効果を発揮しそう
  • 将来的にデータベース以外にもジョブキュー等でコンテナを利用する可能性があるので、投資する価値がありそう

Testcontainersとは

https://testcontainers.com/

Testcontainersは、Dockerコンテナを使用してアプリケーションの統合テストを実行するためのツールです。JavaだけでなくGolangやNode.jsなど、複数の言語で利用できるようになっています。

動作の仕組みとしては、公式サイトにある図がわかりやすいです。

https://testcontainers.com/getting-started/ より

https://testcontainers.com/getting-started/ より

これを我々のテストの文脈に読み替えると、以下のようになります。

  1. テストの実施前に、Testcontainersを呼び出して、テスト用のデータベースコンテナを起動させます。
  2. テストを実行します。ここで1点注意が必要で、テスト実行前に、データベースの接続情報を、Testcontainersが起動したコンテナに向けるように設定を一時的に切り替えます。
  3. テストが終了すると、Testcontainersが起動したデータベースコンテナを片付けてくれます。

Testcontainersのコンテナ起動のポイント

Testcontainersがコンテナを起動するためには、Testcontainersが、macOS(host OS)上で起動しているDockerサーバ(以後Dockerホストと呼ぶ)に対して「このコンテナを起動して」という指示を出せる必要があります。これがVSCode Dev ContainersでTestcontainersを利用する上で大事なポイントになります。

VSCode Dev Containers上でTestcontainersを使う際に起こったエラー

Testcontainersを検証する中で、VSCode Dev Containers上だとうまく動かないポイントがありました。具体的には以下のようなエラーが出ました。

Dockerホストに接続するソケットが見つからない

2023-06-27T14:59:36.255Z ERROR 1134 --- [           main] o.t.d.DockerClientProviderStrategy       : Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
        UnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (Could not find unix domain socket). Root cause NoSuchFileException (/var/run/docker.sock)
        DockerDesktopClientProviderStrategy: failed with exception NullPointerException (Cannot invoke "java.nio.file.Path.toString()" because the return value of "org.testcontainers.dockerclient.DockerDesktopClientProviderStrategy.getSocketPath()" is null)As no valid configuration was found, execution cannot continue.
See https://www.testcontainers.org/on_failure.html for more details.

これは、Testcontainersが、Dockerホストのソケットを見つけられないので、コンテナ起動の指示が出せないというエラーでした。

私達は docker-compose.ymlで開発用コンテナとデータベースコンテナを起動しています。

VSCode Dev Containersが用意する docker-compose.yml のデフォルトの設定だと、Dockerホストに対するソケットが、開発用コンテナ側にマウントされていないため、上記のようなエラーが出ていました。

この問題は、 docker-compose.yml のうち、開発用コンテナに /var/run/docker.sock をマウントすることで解決しました。

# https://github.com/ryan5500/spring-boot-and-testcontainers-on-devcontainer-example/blob/main/.devcontainer/docker-compose.yml から抜粋
services:
  app:

    ...

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

起動したコンテナの疎通確認ができない

上記の問題を解決した後、他のエラーが出ました。

# mvn testでのテスト実行ログから、testcontainerの起動部分を一部抜粋
2023-06-27T14:31:19.254Z  INFO 8850 --- [           main] tc.testcontainers/ryuk:0.5.1             : Container testcontainers/ryuk:0.5.1 is starting: ceb92b9779fc918b4bdbc3d3285ff467b5e177b6d1a9b8b34c7972ae15695972
2023-06-27T14:31:19.585Z  INFO 8850 --- [           main] tc.testcontainers/ryuk:0.5.1             : Container testcontainers/ryuk:0.5.1 started in PT7.772355754S
2023-06-27T14:31:19.591Z  INFO 8850 --- [           main] o.t.utility.RyukResourceReaper           : Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2023-06-27T14:31:19.591Z  INFO 8850 --- [           main] org.testcontainers.DockerClientFactory   : Checking the system...
2023-06-27T14:31:19.592Z  INFO 8850 --- [           main] org.testcontainers.DockerClientFactory   : ✔︎ Docker server version should be at least 1.6.0
2023-06-27T14:31:19.593Z  INFO 8850 --- [           main] tc.postgres:14.5                         : Creating container for image: postgres:14.5
2023-06-27T14:31:19.617Z  INFO 8850 --- [           main] tc.postgres:14.5                         : Container postgres:14.5 is starting: d9b6b5fb8e508fca30313c3a98611e4a315d1a5e1b8372af712c4f2e4f501a3a
2023-06-27T14:32:19.865Z ERROR 8850 --- [           main] tc.postgres:14.5                         : Could not start container

java.lang.IllegalStateException: Wait strategy failed. Container exited with code 1

このログは、Testcontainersで最初に起動する、コンテナ片付け用のコンテナ「ryuk」が起動し、その後PostgreSQLのコンテナを起動しようとするも失敗する、というエラーです。

裏側でDocker Desktopのダッシュボードを見ると、PostgreSQLのコンテナは起動していて、死活確認ができないという状況のように見えました。

そこで、 docker-compose.yml に以下の設定を追記することで、この問題を解決できました。

# https://github.com/ryan5500/spring-boot-and-testcontainers-on-devcontainer-example/blob/main/.devcontainer/docker-compose.yml から抜粋
services:
  app:

    ...
    
    # Dockerコンテナ内から、host.docker.internalドメインで、Dockerホストマシン側にアクセスできるようにする。
    # refer: https://docs.docker.com/desktop/mac/networking/#use-cases-and-workarounds
    extra_hosts:
      - 'host.docker.internal:host-gateway'

    environment:
      # for testcontainer-java
      # refer: https://java.testcontainers.org/features/configuration/#customizing-docker-host-detection
      - TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal

extra_hostsに上記設定を追加することで、開発用コンテナ内部からは host.docker.internal というドメインで、Dockerホスト側に接続できるようになります。

これを、 TESTCONTAINERS_HOST_OVERRIDE 環境変数に設定することで、Testcontainersがコンテナの死活確認をする際の接続先を指定することができます。

サンプルのリポジトリ

上記2点の問題を解決すると、Dev Containers上でTestcontainersを用いたテストを行えるようになりました。

こちらに、VSCode Dev Containersで、Spring Boot + Testcontainersを用いたテストが実行できるサンプルアプリを用意しましたので、他の実装部分について気になる方はご確認ください。

https://github.com/ryan5500/spring-boot-and-testcontainers-on-devcontainer-example

その他

NstockではサーバサイドではJavaで書いていますが、一部運用ツールやバッチ処理などにGolangを利用しています。

Golang用に Testcontainers for Go があり、バッチ処理のテストを書く際にも利用しています。

今後、Spring Boot 3系からはTestcontainersをテスト時に利用するコードが書きやすい枠組みが導入されているので、そちらにもチャレンジしていきたいです。

まとめ

Testcontainers (Java版) をVSCode Dev Containers上から利用する場合は、docker compose設定ファイルに以下の設定を追加します。

  • Testcontainersがコンテナ起動の指示を出すため
    • volumes/var/run/docker.sock:/var/run/docker.sock を追加する
  • テスト時に開発用コンテナ内からTestcontainersが起動したコンテナに接続するため
    • extra_hostshost.docker.internal:host-gatewayを追加する
    • environmentTESTCONTAINERS_HOST_OVERRIDE=host.docker.internalを追加する

サンプルリポジトリ: https://github.com/ryan5500/spring-boot-and-testcontainers-on-devcontainer-example

Nstockではエンジニアを募集しています!
👨‍👩‍👧‍👦カジュアル面談から気軽にお話しましょう🤞

https://speakerdeck.com/nstock/we-are-hiring
https://nstock.co.jp/recruit

Nstock Tech Blog

Discussion