पाठ 21 / 25

Testing with ExUnit

Write unit, context, controller and LiveView tests.

Testing is built in

ExUnit ships with Elixir. Test files end in _test.exs, modules use ExUnit.Case, and tests are written with test "description" do ... end and assert, refute and assert_raise. async: true runs test modules concurrently, which is safe for pure code and, with the Ecto SQL Sandbox, for database tests too, because each test runs in its own transaction that is rolled back. describe groups tests, setup prepares context, and tags (@tag :slow) select or exclude tests. Doctests (doctest Shop.Pricing) run the examples in your @doc strings so documentation stays correct. assert_receive checks that a message arrives, ideal for processes and PubSub. Phoenix generates DataCase, ConnCase (controller tests with get(conn, ~p"/api/products") and json_response/2) and Phoenix.LiveViewTest (live/2, render_click, render_submit and has_element?). Mox creates mocks for behaviours, keeping test doubles honest. StreamData adds property-based testing. Run tests with mix test, filter with mix test test/shop/cart_test.exs:42, and rerun failures with --failed.

Unit, process and LiveView tests

assert, assert_receive and LiveViewTest helpers.

defmodule Shop.PricingTest do
  use ExUnit.Case, async: true
  doctest Shop.Pricing

  describe "with_gst/1" do
    test "adds 18% GST rounded to the nearest paisa" do
      assert Shop.Pricing.with_gst(10_000) == 11_800
    end

    test "rejects negative amounts" do
      assert_raise FunctionClauseError, fn -> Shop.Pricing.with_gst(-1) end
    end
  end
end

defmodule Shop.InventoryTest do
  use ExUnit.Case, async: true

  test "reserving stock broadcasts the new level" do
    Phoenix.PubSub.subscribe(Shop.PubSub, "stock:pen")
    assert {:ok, 8} = Shop.Inventory.reserve("pen", 2)
    assert_receive {:stock_changed, 8}
  end
end

defmodule ShopWeb.StockLiveTest do
  use ShopWeb.ConnCase, async: true
  import Phoenix.LiveViewTest

  test "reserving updates the page", %{conn: conn} do
    {:ok, view, html} = live(conn, ~p"/stock/pen")
    assert html =~ "In stock"

    view |> form("form", %{qty: "3"}) |> render_submit()
    assert render(view) =~ "Reserved 3"
  end
end

async: true by default

Most ExUnit tests can run with async: true. Tests that change global state (application environment, named processes, the file system) should use async: false, or better, be refactored to avoid global state.

त्वरित जाँच: What does assert_receive check?

  • That a function returns true
  • That a file exists
  • That the test process receives a matching message within a timeout
  • That a test is async
Answer

That the test process receives a matching message within a timeout — assert_receive waits for a message matching a pattern.