Mox Documentation

repository·main·Indexed 23 days ago

https://github.com/dashbitco/mox

Mox is an Elixir library for defining concurrent mocks based on explicit behaviours rather than ad-hoc module generation. It provides tools for setting expectations with `expect/3` and `expect/4`, creating stubs via `stub/3` and `stub_with/2`, and denying calls with `deny/3`. The library supports both Private Mode for concurrent async tests and Global Mode for multi-process setups, including a mechanism to grant child processes access to expectations using `Mox.allow/3`.

Tokens
2.3K
Snippets
8
Records
14
Agent score
78%

What's inside Mox

  1. How Mox works: The Mocking Workflow

    main

    Mox follows a specific pattern to enable concurrent, behaviour-based mocking:

    1. Define a Behaviour: Create a module that defines the contract (the functions and their signatures) using @callback.
    2. Provide an Implementation: Create a real module that implements that behaviour for production use.
    3. Implement a Switch: In your application code, do not call the implementation directly. Instead, fetch the implementation module from an application configuration (e.g., Application.get_env/2). This allows you to swap the real implementation for a mock during tests.
    4. Define the Mock: In your test_helper.exs, use Mox.defmock/2 to create a mock module based on your behaviour, and then update your application configuration to point to this new mock module.
    5. Use expect/3 in Tests: In your test files, use expect/3 to define how the mock should respond to specific arguments and to assert that the correct arguments were received.
  2. Ensure Mox is started

    main

    Mox should start automatically. However, if your mix.exs uses the :applications key in def application instead of :extra_applications, you must manually ensure Mox is started in your test/test_helper.exs:

    Application.ensure_all_started(:mox)
  3. Run the Mox barebones example

    main

    The barebones_with_setup example demonstrates how to use Mox to mock an HTTP call during testing and how to switch to a real implementation during interactive development.

    Testing Mode

    To verify that the mock is working (i.e., the HTTP call is intercepted and not actually made), run the test suite:

    mix test

    Interactive/Real Mode

    To run the application with the actual implementation (making real HTTP requests), start the project in IEx and call the context function:

    iex -S mix
    # Inside IEx:
    MoxExample.post_name("Mox")
  4. How Mox handles concurrency and process ownership

    main

    Mox is designed for concurrent testing. It uses a concept of ownership to manage expectations.

    Private Mode & Async Tests

    In private mode, expectations are owned by the process that defines them. Because of this, you can run many tests in parallel using the same mock module without interference. If a child process needs to call the mock, you must use Mox.allow/3 to grant it access to the owner's expectations.

    Global Mode

    In global mode, the mock is shared across all processes. This simplifies testing when many different processes need to interact with the same mock, but it forces you to run tests sequentially (no async: true).

    Avoiding Race Conditions

    When calling a mock from a different process (e.g., via spawn), the test process might call verify!/1 before the spawned process actually executes the mock call. To prevent this, "sync up" with the process by sending a message from the mock's expectation back to the test process:

    # Syncing with a spawned process
    test "calling a mock from a different process" do
      parent = self()
      ref = make_ref()
    
      expect(MyApp.MockWeatherAPI, :temp, fn _loc ->
        send(parent, {ref, :temp})
        {:ok, 30}
      end)
    
      spawn(fn -> MyApp.HumanizedWeather.temp({50.06, 19.94}) end)
    
      # Wait for the mock to actually be called before verifying
      assert_receive {^ref, :temp}
    
      verify!()
    end
  5. Verify mocks on exit with `setup :verify_on_exit!`

    main

    To ensure that all expected calls to a mock actually occurred, include setup :verify_on_exit! in your ExUnit.Case setup. This will cause the test to fail if any expect calls were not satisfied.

    defmodule BoundTest do
      use ExUnit.Case
    
      import Mox
    
      setup :verify_on_exit!
    
      # ... tests ...
    end
  6. Define a mock with `Mox.defmock/2`

    main

    Use Mox.defmock/2 to create a mock module based on an existing behaviour. This is typically done in test/test_helper.exs so the mock is available for all tests.

    Mox.defmock(WeatherBehaviourMock, for: WeatherBehaviour)
  7. Set expectations with `expect/3`

    main

    The expect/3 function allows you to define how a mock should behave when a specific function is called. You can use a function as the third argument to perform assertions on the input arguments and to determine the return value of the mock call.

    expect(WeatherBehaviourMock, :get_weather, fn args ->
      # Assert on the arguments
      assert args == "Chicago"
    
      # Define the return value
      {:ok, %{body: "Some html with weather data"}}
    end)
  8. Deny calls with Mox.deny/3

    main
    Use Mox.deny/3 to ensure that a specific function in a mock is never called. It is equivalent to expect(mock, name, 0, ...). Note that deny/3 will remove any existing stub/3 for that function/arity.
  9. Set expectations with Mox.expect/4

    main

    Use Mox.expect/4 to define how many times a specific function in a mock should be called and what it should return.

    Behavior:

    • If you call expect/4 multiple times for the same function/arity, you can define different behaviors for each successive call (e.g., failing twice then succeeding once).
    • Calling expect/4 for a function/arity will remove any previously defined stub/3 for that same function/arity.
    • If n is 0, it acts as a denial (ensuring the function is not called).
    # Expect :get_temp/1 to be called once
    expect(MockWeatherAPI, :get_temp, fn _ -> {:ok, 30} end)
    
    # Expect :get_temp/1 to be called exactly five times
    expect(MockWeatherAPI, :get_temp, 5, fn _ -> {:ok, 30} end)
    
    # Expect :get_temp/1 to be called zero times
    expect(MockWeatherAPI, :get_temp, 0, fn _ -> {:ok, 30} end)
    
    # Sequence of calls: fail twice, then succeed once
    MockWeatherAPI
      |> expect(:get_temp, 2, fn _loc -> {:error, :unreachable} end)
      |> expect(:get_temp, 1, fn _loc -> {:ok, 30} end)
  10. Configure Mox mode (Private vs Global)

    main

    Mox operates in two modes to balance concurrency and ease of use:

    1. Private Mode (Default): Mocks are scoped to the process that defines them. This allows multiple tests to run concurrently (async: true) using the same mock module, as long as they are in different processes.
    2. Global Mode: Any process can consume mocks and stubs defined in the test process. This is easier for complex multi-process setups but cannot be used with async: true in ExUnit.

    Helper Functions:

    • Mox.set_mox_private/1: Sets mode to private.
    • Mox.set_mox_global/1: Sets mode to global (raises error if the test is async: true).
    • Mox.set_mox_from_context/1: Automatically chooses the correct mode based on whether the test is async: true.
    # In a test setup block
    setup :set_mox_from_context
    setup :verify_on_exit!
  11. Verify mocks with Mox.verify!/1 and Mox.verify_on_exit!/1

    main

    Mox requires you to verify that your expectations were met.

    • Mox.verify!/1: Verifies that all expectations set by the current process for the specified mock (or all mocks if :all is used) have been fulfilled.
    • Mox.verify_on_exit!/1: A convenience function for ExUnit. When used in a setup block, it automatically verifies expectations when the test exits.

    Best Practice: Use setup :verify_on_exit! in your test files or case templates to ensure you never forget to verify your mocks.