View Source Testing

Mix.install([:bonny, :inflex])

Application.put_env(:bonny, :operator_name, "livebook-operator")

ExUnit.start(autorun: false)

The Controller to Test

In this guide we're going to create a simple controller and write a test for it. The controller's custom resource is called ConfigMapToPluralize and we expect the controller to map the resource to a ConfigMap on the cluster. The ConfigMap should contain the same fields as the ConfigMapToPluralize but the fielt's values are pluralized.

Example

For the following ConfigMapToPluralize resource:

apiVersion: example.com/v1
kind: ConfigMapToPluralize
metadata:
  name: foo
  namespace: default
data:
  first: House
  second: Hero

the controller will create the following ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: foo
  namespace: default
data:
  first: Houses
  second: Heroes

The CRD API V1

Let's define the API version V1 for the ConfigMapToPluralize CRD. It defines a schema with a property .data which is an object allowing for arbitrary fields with string values.

defmodule V1.ConfigMapToPluralize do
  use Bonny.API.Version,
    hub: true

  def manifest() do
    struct!(
      defaults(),
      schema: %{
        openAPIV3Schema: %{
          type: :object,
          properties: %{
            data: %{
              type: :object,
              additionalProperties: %{
                type: :string
              },
              "x-kubernetes-preserve-unknown-fields": true
            }
          }
        }
      }
    )
  end
end

The Controller

The ConfigMapToPluralizeController handles :add and :modify and :reconcile events through the same function where the ConfigMap with pluralized field values is created and registered as descendant.

defmodule ConfigMapToPluralizeController do
  use Bonny.ControllerV2

  step :handle_event

  def handle_event(%Bonny.Axn{action: action} = axn, _opts)
      when action in [:add, :modify, :reconcile] do
    %Bonny.Axn{resource: resource} = axn

    name = K8s.Resource.FieldAccessors.name(resource)
    namespace = K8s.Resource.FieldAccessors.namespace(resource)

    new_data =
      resource
      |> Map.get("data")
      |> Enum.map(fn {key, value} ->
        {key, Inflex.pluralize(value)}
      end)
      |> Enum.into(%{})

    cm = %{
      "apiVersion" => "v1",
      "kind" => "ConfigMap",
      "metadata" => %{
        "name" => name,
        "namespace" => namespace
      },
      "data" => new_data
    }

    axn
    |> register_descendant(cm)
    |> success_event()
  end

  def handle_event(axn, _opts) do
    # since we added the owner reference above, there's nothing to do here.
    # Kubernetes will delete the referencing objects i.e. the ConfigMap for us.
    success_event(axn)
  end
end

Testing the controller

Testing a Bonny controller is similar to testing a Phoenix controller. Controllers use the %Bonny.Axn{} token to register descending resources, update resource status and registering Kubernetes events. In our test, we need to assert the state of the returned Bonny.Axn{} struct is as expected.

Helper Module Bonny.Axn.Test

Bonny.Axn.Test is a helper module for tests. It provides a function axn/2 to create a %Bonny.Axn{} token and other helper functions which are imported to your test when using the helper module.

defmodule ConfigMapToPluralizeControllerTest do
  use ExUnit.Case, async: true
  use Bonny.Axn.Test

  #  Module Under Test
  alias ConfigMapToPluralizeController, as: MUT

  setup do
    cm_to_pluralize = %{
      "apiVersion" => "example.com/v1",
      "kind" => "ConfigMapToPluralize",
      "metadata" => %{
        "namespace" => "default",
        "name" => "test-1"
      },
      "data" => %{
        "first" => "House",
        "second" => "Hero"
      }
    }

    [
      axn: axn(:add, resource: cm_to_pluralize, conn: %K8s.Conn{})
    ]
  end

  test "registers descending ConfigMap with pluralized fields", %{axn: axn} do
    assert [cm] = axn |> MUT.call([]) |> descendants()
    assert "Houses" == cm["data"]["first"]
    assert "Heroes" == cm["data"]["second"]
  end

  test "registers a success event", %{axn: axn} do
    assert [event] = axn |> MUT.call([]) |> events()
    assert :Normal == event.event_type
  end
end

ExUnit.run()

Integration Testing

You can also test your code against a real Kubernetes cluster setup locally (e.g. with k3d) and in your CI. You will need to setup a %K8s.Conn{} defines the connection your cluster but is oonly used for the integration tests. Have a look at the follwing files in the Bonny repo:

  • test/support/integration_helper.ex - defines a conn/0 function
  • test/bonny/controller_v2_integration_test.exs - implements integration tests using the helper above.