Skip to main content

lib/exunit.ex

# SPDX-License-Identifier: MIT
# Original by: Marco Dallagiacoma @ 2023 in https://github.com/dallagi/excontainers
# Modified by: Jarl André Hübenthal @ 2023
defmodule TestcontainerEx.ExUnit do
  @moduledoc """
  Convenient macros to run containers within ExUnit tests.
  """
  import ExUnit.Callbacks

  @doc """
  Creates and manages the lifecycle of a container within ExUnit tests.

  When the `:shared` option is set to `true`, the container is created once for all tests in the module. It initializes the container before any test is run and keeps it running across multiple tests. This is useful for scenarios where the cost of setting up and tearing down the container is high, or the tests are read-only and won't change the container's state.

  When the `:shared` option is omitted or set to `false`, a new container is created for each individual test, ensuring a clean state for each test case. The container is removed after each test finishes.

  ## Parameters

    * `name`: The key that should be used to reference the container in test cases.
    * `config`: Configuration necessary for initializing the container.
    * `options`: Optional keyword list. Supports the following options:
      * `:shared` - If set to `true`, the container is shared across all tests in the module. If `false` or omitted, a new container is used for each test.

  ## Examples

  To create a new container for each test:

      defmodule MyTest do
        use ExUnit.Case

        alias TestcontainerEx.Container

        container :my_container, %Container{image: "my_image"}
        # ...
      end

  To share a container across all tests in the module:

      defmodule MySharedTest do
        use ExUnit.Case

        alias TestcontainerEx.Container

        container :my_shared_container, %Container{image: "my_shared_image"}, shared: true
        # ...
      end

  ## Notes

    * The macro sets up the necessary ExUnit callbacks to manage the container's lifecycle.
    * It ensures the `Connection` and `Reaper` are started before initializing a container.
    * In the case of shared containers, be mindful that tests can affect the container's state, potentially leading to interdependencies between tests.
  """
  defmacro container(name, config, options \\ []) do
    run_block =
      quote do
        {:ok, container} = TestcontainerEx.start_container(unquote(config))
        ExUnit.Callbacks.on_exit(fn -> TestcontainerEx.stop_container(container.container_id) end)
        {:ok, %{unquote(name) => container}}
      end

    case Keyword.get(options, :shared, false) do
      true ->
        quote do
          setup_all do
            unquote(run_block)
          end
        end

      _ ->
        quote do
          setup do
            unquote(run_block)
          end
        end
    end
  end

  @doc """
  Creates and manages the lifecycle of a Docker Compose environment within ExUnit tests.

  When the `:shared` option is set to `true`, the compose environment is created once for all
  tests in the module. When omitted or set to `false`, a new compose environment is created
  for each individual test.

  ## Parameters

    * `name`: The key that should be used to reference the compose environment in test cases.
    * `config`: A `%TestcontainerEx.DockerCompose{}` struct with the compose configuration.
    * `options`: Optional keyword list. Supports the following options:
      * `:shared` - If set to `true`, the compose environment is shared across all tests.

  ## Examples

      defmodule MyComposeTest do
        use ExUnit.Case

        alias TestcontainerEx.DockerCompose

        compose :my_env, DockerCompose.new("test/fixtures")
        # ...
      end
  """
  defmacro compose(name, config, options \\ []) do
    run_block =
      quote do
        {:ok, env} = TestcontainerEx.start_compose(unquote(config))
        ExUnit.Callbacks.on_exit(fn -> TestcontainerEx.stop_compose(env) end)
        {:ok, %{unquote(name) => env}}
      end

    case Keyword.get(options, :shared, false) do
      true ->
        quote do
          setup_all do
            unquote(run_block)
          end
        end

      _ ->
        quote do
          setup do
            unquote(run_block)
          end
        end
    end
  end
end