Testcontainers for Elixir

repository·main·Indexed 41 days ago

https://github.com/testcontainers/testcontainers-elixir

An Elixir library for managing lightweight, throwaway instances of common services like databases or browsers using Docker containers. It provides a generic Container API, predefined modules like RedisContainer, and integration with ExUnit via the Testcontainers.ExUnit macro. Includes a Mix task `mix testcontainers.run` to wrap tasks with containerized infrastructure and a Compose CLI for managing Docker Compose lifecycles.

Tokens
13.1K
Snippets
58
Records
67
Agent score
51%

What's inside Testcontainers Elixir

  1. Use generic and predefined containers

    main

    You can manage containers using a generic API or use predefined modules like RedisContainer which include optimized waiting strategies and defaults.

    Generic Container API

    Define everything manually using the %Testcontainers.Container{} struct:

    {:ok, _} = Testcontainers.start_link()
    config = %Testcontainers.Container{image: "redis:5.0.3-alpine"}
    {:ok, container} = Testcontainers.start_container(config)

    Predefined Containers

    Use specialized modules for common services. You can also override the image or check image using builder patterns:

    {:ok, _} = Testcontainers.start_link()
    
    # Using default Redis configuration
    config = Testcontainers.RedisContainer.new()
    {:ok, container} = Testcontainers.start_container(config)
    
    # Using an alternative image (e.g., Valkey)
    config =
      Testcontainers.RedisContainer.new()
      |> Testcontainers.RedisContainer.with_image("valkey/valkey:latest")
      |> Testcontainers.RedisContainer.with_check_image("valkey/valkey")
    {:ok, container} = Testcontainers.start_container(config)
    config = Testcontainers.RedisContainer.new()
    {:ok, container} = Testcontainers.start_container(config)
  2. Install Testcontainers

    main

    To add Testcontainers to your Elixir project, follow these steps:

    1. Add testcontainers to your mix.exs dependencies, restricted to :test and :dev environments:
    def deps do
      [
        {:testcontainers, "~> X.XX", only: [:test, :dev]}
      ]
    end

    (Replace X.XX with the current version).

    1. Run mix deps.get.

    2. Initialize Testcontainers in your test/test_helper.exs:

    Testcontainers.start_link()
    def deps do
      [
        {:testcontainers, "~> X.XX", only: [:test, :dev]}
      ]
    end
  3. Start a Phoenix server

    main

    To start a Phoenix server in a local development environment, follow these steps:

    1. Install and set up dependencies using mix setup.
    2. Start the Phoenix endpoint using mix phx.server or start an interactive Elixir shell (IEx) with the server running using iex -S mix phx.server.

    Once started, the server is accessible at http://localhost:4000.

    mix setup
    mix phx.server
    # OR
    iex -S mix phx.server
  4. Setup Testcontainers on Windows

    main

    There are two ways to use Testcontainers on Windows:

    Download and install Testcontainers Desktop. This provides a managed environment where everything is pre-configured.

    2. Native Windows Setup (Experimental/Not officially supported)

    If running natively with Elixir/Erlang, you must:

    1. Install Visual Studio 2022 with Desktop development with C++.
    2. In Docker settings, enable "Expose daemon on tcp://localhost:2375 without TLS".
    3. Set the DOCKER_HOST environment variable:
      • PowerShell: $Env:DOCKER_HOST = "tcp://localhost:2375"
      • CMD: set DOCKER_HOST = "tcp://localhost:2375"
  5. Configure Database URL in `runtime.exs`

    main

    To allow Testcontainers to inject the containerized database connection into your application, configure your config/runtime.exs to read the DATABASE_URL environment variable provided by the testcontainers.run task.

    # config/runtime.exs
    
    if config_env() in [:dev, :test] do
      if url = System.get_env("DATABASE_URL") do
        config :my_app, MyApp.Repo,
          url: url,
          pool: Ecto.Adapters.SQL.Sandbox,
          show_sensitive_data_on_connection_error: true,
          pool_size: 10
      end
    end
    if config_env() in [:dev, :test] do
      if url = System.get_env("DATABASE_URL") do
        config :my_app, MyApp.Repo,
          url: url,
          pool: Ecto.Adapters.SQL.Sandbox,
      end
    end
  6. How MongoContainer configuration works

    main

    A MongoContainer is a configuration struct used to define how a MongoDB instance should be launched. It implements the Testcontainers.DatabaseBehaviour and Testcontainers.ContainerBuilder protocols.

    When you call ContainerBuilder.build/2 with a MongoContainer config, the library translates your high-level settings (like user and database) into Docker-specific instructions, such as environment variables and command-line wait strategies. This abstraction allows you to focus on database credentials and connectivity rather than low-level Docker orchestration.

  7. Configure a RabbitMQ container

    main

    Use Testcontainers.RabbitMQContainer.new/0 to create a default RabbitMQ configuration and use the with_* functions to override settings like image, port, credentials, and virtual hosts.

    Note that the default starting command includes chmod 400 /var/lib/rabbitmq/.erlang.cookie, which is required for the rabbitmq-diagnostics check_running waiting strategy to work correctly.

    Common configuration functions:

    • with_image(config, image)
    • with_port(config, port)
    • with_wait_timeout(config, timeout)
    • with_username(config, username)
    • with_password(config, password)
    • with_virtual_host(config, vhost)
    • with_cmd(config, cmd)
    • with_reuse(config, boolean)
    • with_force_reuse(config)
    config = 
      Testcontainers.RabbitMQContainer.new()
      |> Testcontainers.RabbitMQContainer.with_image("rabbitmq:xyz")
      |> Testcontainers.RabbitMQContainer.with_port(1111)
      |> Testcontainers.RabbitMQContainer.with_username("rabbitmq")
      |> Testcontainers.RabbitMQContainer.with_password("rabbitmq")
  8. Use KafkaContainer to manage Kafka instances

    main

    Testcontainers.KafkaContainer provides functionality for creating and managing Kafka containers using the official apache/kafka Docker image in KRaft mode (no Zookeeper required).

    By default, it selects a random host port between 29000-29999 to avoid conflicts, as the Kafka image requires the advertised listener address to be known at startup.

    To use it, create a configuration with KafkaContainer.new/0, then start it using Testcontainers.start_container/1. You can retrieve the connection string using KafkaContainer.bootstrap_servers/1.

    config = KafkaContainer.new()
    {:ok, container} = Testcontainers.start_container(config)
    
    # Get the bootstrap server address for your client
    bootstrap_servers = KafkaContainer.bootstrap_servers(container)
  9. Configure an EMQX container

    main

    Use Testcontainers.EmqxContainer to define the configuration for an EMQX container. You can initialize a default configuration using new/0 and then use various with_* functions to override settings like the Docker image, ports, and reuse behavior. To actually start the container, pass the configuration to a ContainerBuilder (typically via Testcontainers.Container.start/1 or similar orchestration logic in your project).

    config = Testcontainers.EmqxContainer.new()
             |> Testcontainers.EmqxContainer.with_image("emqx:5.6.0")
             |> Testcontainers.EmqxContainer.with_ports(1883, 8883, 8083, 8084, 18083)
             |> Testcontainers.EmqxContainer.with_reuse(true)
    
    # The config is then used to build/start the container
  10. Configure ToxiproxyEx to use a ToxiproxyContainer

    main

    If you are using the ToxiproxyEx library, you can automatically configure it to point to your running Toxiproxy container using configure_toxiproxy_ex/1. This sets the :toxiproxy_ex application environment :host to the container's API URL.

    {:ok, toxiproxy} = Testcontainers.start_container(ToxiproxyContainer.new())
    :ok = Testcontainers.ToxiproxyContainer.configure_toxiproxy_ex(toxiproxy)
    
    # Now ToxiproxyEx is ready to use
    ToxiproxyEx.get!("my_proxy")
    {:ok, toxiproxy} = Testcontainers.start_container(ToxiproxyContainer.new())
    :ok = Testcontainers.ToxiproxyContainer.configure_toxiproxy_ex(toxiproxy)
    
    # Now ToxiproxyEx will use this container
    ToxiproxyEx.get!("my_proxy") |> ToxiproxyEx.down!(fn -> ... end)
  11. Configure a CephContainer

    main

    Use Testcontainers.CephContainer to create and customize configurations for Ceph containers. You can start with a default configuration using CephContainer.new() and then use various with_* functions to override settings like the image, authentication keys, bucket name, port, and wait timeout.

    Commonly, you will pass this configuration to ContainerBuilder.build/2 to obtain a running %Container{} instance.

    config = 
      CephContainer.new()
      |> CephContainer.with_image("quay.io/ceph/alternative")
      |> CephContainer.with_access_key("my_access_key")
      |> CephContainer.with_secret_key("my_secret_key")
      |> CephContainer.with_bucket("my_bucket")
      |> CephContainer.with_port(8081)
    
    built_container = ContainerBuilder.build(config, [])