├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── examples └── barebones_with_setup │ ├── .formatter.exs │ ├── README.md │ ├── config │ ├── config.exs │ ├── dev.exs │ └── test.exs │ ├── lib │ └── mox_example.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── mox_example_test.exs │ ├── support │ └── mocks.ex │ └── test_helper.exs ├── lib ├── mox.ex └── mox │ └── application.ex ├── mix.exs ├── mix.lock └── test ├── cluster_test.exs ├── mox_test.exs ├── support ├── behaviours.ex └── mocks.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "lib/**/*.ex", 4 | "mix.exs", 5 | "test/**/*.{ex,exs}" 6 | ] 7 | ] 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-20.04 12 | env: 13 | MIX_ENV: test 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - pair: 21 | elixir: "1.11" 22 | otp: "21" 23 | - pair: 24 | elixir: "1.18" 25 | otp: "27" 26 | lint: lint 27 | coverage: coverage 28 | steps: 29 | - name: Check out this repository 30 | uses: actions/checkout@v4 31 | 32 | - name: Set up Erlang and Elixir 33 | uses: erlef/setup-beam@v1 34 | with: 35 | otp-version: ${{ matrix.pair.otp }} 36 | elixir-version: ${{ matrix.pair.elixir }} 37 | 38 | - name: Restored cached Mix dependencies (downloaded and compiled) 39 | uses: actions/cache/restore@v4 40 | id: restore-mix-deps-cache 41 | with: 42 | path: | 43 | deps 44 | _build 45 | key: | 46 | ${{ runner.os }}-mix-${{ matrix.pair.elixir }}-${{ matrix.pair.otp }}-${{ hashFiles('**/mix.lock') }}-${{ github.run_id }} 47 | restore-keys: | 48 | ${{ runner.os }}-mix-${{ matrix.pair.elixir }}-${{ matrix.pair.otp }}-${{ hashFiles('**/mix.lock') }}- 49 | ${{ runner.os }}-mix-${{ matrix.pair.elixir }}-${{ matrix.pair.otp }}- 50 | 51 | - name: Fetch and compile Mix dependencies 52 | run: mix do deps.get --check-locked, deps.compile 53 | 54 | - name: Cache compiled Mix dependencies 55 | uses: actions/cache/save@v4 56 | with: 57 | path: | 58 | deps 59 | _build 60 | key: ${{ steps.restore-mix-deps-cache.outputs.cache-primary-key }} 61 | 62 | - name: Check for formatted files 63 | run: mix format --check-formatted 64 | if: ${{ matrix.lint }} 65 | 66 | - name: Check for unused dependencies 67 | run: mix deps.unlock --check-unused 68 | if: ${{ matrix.lint }} 69 | 70 | - name: Check compilation warnings 71 | run: mix compile --warnings-as-errors 72 | if: ${{ matrix.lint }} 73 | 74 | # We always need to run tests because code coverage forces us to skip some 75 | # tests, so better to run normal tests on the whole matrix and the coverage 76 | # only on the latest version. 77 | - name: Run tests 78 | run: mix test 79 | 80 | - name: Run tests with coverage 81 | if: ${{ matrix.coverage }} 82 | run: mix coveralls.github --exclude fails_on_coverage 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | /examples/*/_build/ 4 | 5 | # If you run "mix test --cover", coverage assets end up here. 6 | /cover/ 7 | 8 | # The directory Mix downloads your dependencies sources to. 9 | /deps/ 10 | /examples/*/deps/ 11 | 12 | # Where 3rd-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | # Also ignore archive artifacts (built via "mix archive.build"). 22 | *.ez 23 | 24 | # Ignore package tarball (built via "mix hex.build"). 25 | mox-*.tar 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.2.0 (2024-08-04) 4 | 5 | This release is mostly about reducing the complexity of Mox by switching its ownership implementation to use the new [nimble_ownership library](https://github.com/dashbitco/nimble_ownership). 6 | 7 | ### Enhancements 8 | 9 | * Add `Mox.deny/3`. 10 | * Optimize `Mox.stub_with/2`. 11 | 12 | ## v1.1.0 (2023-09-20) 13 | 14 | ### Enhancements 15 | 16 | * Support testing in a cluster 17 | * Support a function to retrieve the PID to allow in `Mox.allow/3` 18 | 19 | ## v1.0.2 (2022-05-30) 20 | 21 | ### Bug fix 22 | 23 | * Use `Code.ensure_compiled!` to support better integration with the Elixir compiler 24 | 25 | ## v1.0.1 (2020-10-15) 26 | 27 | ### Bug fix 28 | 29 | * Fix race condition for when the test process terminates and a new one is started before the DOWN message is processed 30 | 31 | ## v1.0.0 (2020-09-25) 32 | 33 | ### Enhancements 34 | 35 | * Add `@behaviour` attribute to Mox modules 36 | 37 | ## v0.5.2 (2020-02-20) 38 | 39 | ### Enhancements 40 | 41 | * Warn if global is used with async mode 42 | * Fix compilation warnings 43 | 44 | ## v0.5.1 (2019-05-24) 45 | 46 | ### Enhancements 47 | 48 | * Add `:skip_optional_callbacks` option to `defmock/2` that allows you to optionally skip the definition of optional callbacks. 49 | * Include arguments in `UnexpectedCallError` exceptions 50 | 51 | ## v0.5.0 (2019-02-03) 52 | 53 | ### Enhancements 54 | 55 | * Use `$callers` to automatically use expectations defined in the calling process (`$callers` is set automatically by tasks in Elixir v1.8 onwards) 56 | * Creating an allowance in global mode is now a no-op for convenience 57 | * Support registered process names for allowances 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mox 2 | 3 | [![hex.pm](https://img.shields.io/badge/Package%20on%20hex.pm-informational)](https://hex.pm/packages/mox) 4 | [![hexdocs.pm](https://img.shields.io/badge/Documentation-ff69b4)](https://hexdocs.pm/mox) 5 | [![ci](https://github.com/dashbitco/mox/actions/workflows/ci.yml/badge.svg)](https://github.com/dashbitco/mox/actions/workflows/ci.yml) 6 | [![coverage](https://coveralls.io/repos/github/dashbitco/mox/badge.svg?branch=main)](https://coveralls.io/github/dashbitco/mox?branch=main) 7 | 8 | Mox is a library for defining concurrent mocks in Elixir. 9 | 10 | The library follows the principles outlined in ["Mocks and explicit contracts"](https://dashbit.co/blog/mocks-and-explicit-contracts), summarized below: 11 | 12 | 1. No ad-hoc mocks. You can only create mocks based on behaviours 13 | 14 | 2. No dynamic generation of modules during tests. Mocks are preferably defined in your `test_helper.exs` or in a `setup_all` block and not per test 15 | 16 | 3. Concurrency support. Tests using the same mock can still use `async: true` 17 | 18 | 4. Rely on pattern matching and function clauses for asserting on the 19 | input instead of complex expectation rules 20 | 21 | The goal behind Mox is to help you think and define the contract between the different parts of your application. In the opinion of Mox maintainers, as long as you follow those guidelines and keep your tests concurrent, any library for mocks may be used (or, in certain cases, you may not even need one). 22 | 23 | [See the documentation](https://hexdocs.pm/mox) for more information. 24 | 25 | ## Installation 26 | 27 | Just add `mox` to your list of dependencies in `mix.exs`: 28 | 29 | ```elixir 30 | def deps do 31 | [ 32 | {:mox, "~> 1.0", only: :test} 33 | ] 34 | end 35 | ``` 36 | 37 | Mox should be automatically started unless the `:applications` key is set inside `def application` in your `mix.exs`. In such cases, you need to [remove the `:applications` key in favor of `:extra_applications`](https://elixir-lang.org/blog/2017/01/05/elixir-v1-4-0-released/#application-inference) or call `Application.ensure_all_started(:mox)` in your `test/test_helper.exs`. 38 | 39 | ## Basic Usage 40 | 41 | ### 1) Add behaviour, defining the contract 42 | 43 | ```elixir 44 | # lib/weather_behaviour.ex 45 | defmodule WeatherBehaviour do 46 | @callback get_weather(binary()) :: {:ok, map()} | {:error, binary()} 47 | end 48 | ``` 49 | 50 | ### 2) Add implementation for the behaviour 51 | 52 | ```elixir 53 | # lib/weather_impl.ex 54 | defmodule WeatherImpl do 55 | @moduledoc """ 56 | An implementation of a WeatherBehaviour 57 | """ 58 | 59 | @behaviour WeatherBehaviour 60 | 61 | @impl WeatherBehaviour 62 | def get_weather(city) when is_binary(city) do 63 | # Here you could call an external api directly with an HTTP client or use a third 64 | # party library that does that work for you. In this example we send a 65 | # request using a `httpc` to get back some html, which we can process later. 66 | 67 | :inets.start() 68 | :ssl.start() 69 | 70 | case :httpc.request(:get, {"https://www.google.com/search?q=weather+#{city}", []}, [], []) do 71 | {:ok, {_, _, html_content}} -> {:ok, %{body: html_content}} 72 | error -> {:error, "Error getting weather: #{inspect(error)}"} 73 | end 74 | end 75 | end 76 | ``` 77 | 78 | ### 3) Add a switch 79 | 80 | This can pull from your `config/config.exs`, `config/test.exs`, or, you can have no config as shown below and rely on a default. We also add a function to a higher level abstraction that will call the correct implementation: 81 | 82 | ```elixir 83 | # bound.ex, the main context we chose to call this function from 84 | defmodule Bound do 85 | def get_weather(city) do 86 | weather_impl().get_weather(city) 87 | end 88 | 89 | defp weather_impl() do 90 | Application.get_env(:bound, :weather, WeatherImpl) 91 | end 92 | end 93 | ``` 94 | 95 | ### 4) Define the mock so it is used during tests 96 | 97 | ```elixir 98 | # In your test/test_helper.exs 99 | Mox.defmock(WeatherBehaviourMock, for: WeatherBehaviour) # <- Add this 100 | Application.put_env(:bound, :weather, WeatherBehaviourMock) # <- Add this 101 | 102 | ExUnit.start() 103 | ``` 104 | 105 | ### 5) Create a test and use `expect` to assert on the mock arguments 106 | 107 | ```elixir 108 | # test/bound_test.exs 109 | defmodule BoundTest do 110 | use ExUnit.Case 111 | 112 | import Mox 113 | 114 | setup :verify_on_exit! 115 | 116 | describe "get_weather/1" do 117 | test "fetches weather based on a location" do 118 | expect(WeatherBehaviourMock, :get_weather, fn args -> 119 | # here we can assert on the arguments that get passed to the function 120 | assert args == "Chicago" 121 | 122 | # here we decide what the mock returns 123 | {:ok, %{body: "Some html with weather data"}} 124 | end) 125 | 126 | assert {:ok, _} = Bound.get_weather("Chicago") 127 | end 128 | end 129 | end 130 | ``` 131 | 132 | ## Enforcing consistency with behaviour typespecs 133 | 134 | [Hammox](https://github.com/msz/hammox) is an enhanced version of Mox which automatically makes sure that calls to mocks match the typespecs defined in the behaviour. If you find this useful, see the [project homepage](https://github.com/msz/hammox). 135 | 136 | ## License 137 | 138 | Copyright 2017 Plataformatec \ 139 | Copyright 2020 Dashbit 140 | 141 | Licensed under the Apache License, Version 2.0 (the "License"); 142 | you may not use this file except in compliance with the License. 143 | You may obtain a copy of the License at 144 | 145 | http://www.apache.org/licenses/LICENSE-2.0 146 | 147 | Unless required by applicable law or agreed to in writing, software 148 | distributed under the License is distributed on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 150 | See the License for the specific language governing permissions and 151 | limitations under the License. 152 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "lib/**/*.ex", 4 | "mix.exs", 5 | "test/**/*.{ex,exs}" 6 | ] 7 | ] 8 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/README.md: -------------------------------------------------------------------------------- 1 | # MoxExample 2 | 3 | ## Description 4 | 5 | This example is a barebones example of how to use [Mox](https://github.com/dashbitco/mox) and how to configure it during testing. 6 | 7 | ## How 8 | 9 | Run `mix test` and you'll see a mocked HTTP call tested without making the HTTP call. 10 | 11 | Run `iex -S mix` and then `MoxExample.post_name("Mox")` and you'll see the HTTP request go through! 12 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :mox_example, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:mox_example, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | # this line configures our :example_api to use the ExampleAPIMock defined in 3 | # test/support/mocks.ex 4 | config :mox_example, :example_api, ExampleAPIMock 5 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/lib/mox_example.ex: -------------------------------------------------------------------------------- 1 | defmodule MoxExample do 2 | @example_api Application.get_env( 3 | :mox_example, 4 | :example_api, 5 | ExampleAPI 6 | ) 7 | def post_name(name) do 8 | # run api-post/3 in our currently selected implementation 9 | @example_api.api_post(name, [], []) 10 | end 11 | end 12 | 13 | defmodule ExampleAPI do 14 | alias HTTPoison 15 | @callback api_post(String.t(), [], []) :: {:ok, nil} | {:error, any()} 16 | def api_post(body \\ "", headers, options) do 17 | # if I don't mock this then this is an integration test! 18 | HTTPoison.post("http://example.com", body, headers, options) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MoxExample.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :mox_example, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | elixirc_options: [warnings_as_errors: true], 12 | elixirc_paths: elixirc_paths(Mix.env()) 13 | ] 14 | end 15 | 16 | # Run "mix help compile.app" to learn about applications. 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | # ensure test/support is compiled 24 | defp elixirc_paths(:test), do: ["lib", "test/support"] 25 | defp elixirc_paths(_), do: ["lib"] 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | {:httpoison, "~> 1.4"}, 31 | {:mox, "~> 1.0", only: :test} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, 3 | "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "c2790c9f0f7205f4a362512192dee8179097394400e745e4d20bab7226a8eaad"}, 4 | "httpoison": {:hex, :httpoison, "1.5.0", "71ae9f304bdf7f00e9cd1823f275c955bdfc68282bc5eb5c85c3a9ade865d68e", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "e9d994aea63fab9e29307920492ab95f87339b56fbc5c8c4b1f65ea20d3ba9a4"}, 5 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, 6 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 7 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 8 | "mox": {:hex, :mox, "1.0.0", "4b3c7005173f47ff30641ba044eb0fe67287743eec9bd9545e37f3002b0a9f8b", [:mix], [], "hexpm", "201b0a20b7abdaaab083e9cf97884950f8a30a1350a1da403b3145e213c6f4df"}, 9 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 10 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm", "603561dc0fd62f4f2ea9b890f4e20e1a0d388746d6e20557cafb1b16950de88c"}, 11 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, 12 | } 13 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/test/mox_example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MoxExampleTest do 2 | import Mox 3 | 4 | use ExUnit.Case 5 | doctest MoxExample 6 | 7 | setup :verify_on_exit! 8 | 9 | test "posts name" do 10 | name = "Jim" 11 | 12 | ExampleAPIMock 13 | |> expect(:api_post, fn ^name, [], [] -> {:ok, nil} end) 14 | 15 | assert MoxExample.post_name(name) == {:ok, nil} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/test/support/mocks.ex: -------------------------------------------------------------------------------- 1 | Mox.defmock(ExampleAPIMock, for: ExampleAPI) 2 | -------------------------------------------------------------------------------- /examples/barebones_with_setup/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/mox.ex: -------------------------------------------------------------------------------- 1 | defmodule Mox do 2 | @moduledoc ~S""" 3 | Mox is a library for defining concurrent mocks in Elixir. 4 | 5 | The library follows the principles outlined in 6 | ["Mocks and explicit contracts"](https://dashbit.co/blog/mocks-and-explicit-contracts), 7 | summarized below: 8 | 9 | 1. No ad-hoc mocks. You can only create mocks based on behaviours 10 | 11 | 2. No dynamic generation of modules during tests. Mocks are preferably defined 12 | in your `test_helper.exs` or in a `setup_all` block and not per test 13 | 14 | 3. Concurrency support. Tests using the same mock can still use `async: true` 15 | 16 | 4. Rely on pattern matching and function clauses for asserting on the 17 | input instead of complex expectation rules 18 | 19 | ## Example 20 | 21 | Imagine that you have an app that has to display the weather. At first, 22 | you use an external API to give you the data given a lat/long pair: 23 | 24 | defmodule MyApp.HumanizedWeather do 25 | def display_temp({lat, long}) do 26 | {:ok, temp} = MyApp.WeatherAPI.temp({lat, long}) 27 | "Current temperature is #{temp} degrees" 28 | end 29 | 30 | def display_humidity({lat, long}) do 31 | {:ok, humidity} = MyApp.WeatherAPI.humidity({lat, long}) 32 | "Current humidity is #{humidity}%" 33 | end 34 | end 35 | 36 | However, you want to test the code above without performing external 37 | API calls. How to do so? 38 | 39 | First, it is important to define the `WeatherAPI` behaviour that we want 40 | to mock. And we will define a proxy function that will dispatch to 41 | the desired implementation: 42 | 43 | defmodule MyApp.WeatherAPI do 44 | @callback temp(MyApp.LatLong.t()) :: {:ok, integer()} 45 | @callback humidity(MyApp.LatLong.t()) :: {:ok, integer()} 46 | 47 | def temp(lat_long), do: impl().temp(lat_long) 48 | def humidity(lat_long), do: impl().humidity(lat_long) 49 | defp impl, do: Application.get_env(:my_app, :weather, MyApp.ExternalWeatherAPI) 50 | end 51 | 52 | By default, we will dispatch to MyApp.ExternalWeatherAPI, which now contains 53 | the external API implementation. 54 | 55 | If you want to mock the WeatherAPI behaviour during tests, the first step 56 | is to define the mock with `defmock/2`, usually in your `test_helper.exs`, 57 | and configure your application to use it: 58 | 59 | Mox.defmock(MyApp.MockWeatherAPI, for: MyApp.WeatherAPI) 60 | Application.put_env(:my_app, :weather, MyApp.MockWeatherAPI) 61 | 62 | Now in your tests, you can define expectations with `expect/4` and verify 63 | them via `verify_on_exit!/1`: 64 | 65 | defmodule MyApp.HumanizedWeatherTest do 66 | use ExUnit.Case, async: true 67 | 68 | import Mox 69 | 70 | # Make sure mocks are verified when the test exits 71 | setup :verify_on_exit! 72 | 73 | test "gets and formats temperature and humidity" do 74 | MyApp.MockWeatherAPI 75 | |> expect(:temp, fn {_lat, _long} -> {:ok, 30} end) 76 | |> expect(:humidity, fn {_lat, _long} -> {:ok, 60} end) 77 | 78 | assert MyApp.HumanizedWeather.display_temp({50.06, 19.94}) == 79 | "Current temperature is 30 degrees" 80 | 81 | assert MyApp.HumanizedWeather.display_humidity({50.06, 19.94}) == 82 | "Current humidity is 60%" 83 | end 84 | end 85 | 86 | All expectations are defined based on the current process. This 87 | means multiple tests using the same mock can still run concurrently 88 | unless the Mox is set to global mode. See the "Multi-process collaboration" 89 | section. 90 | 91 | One last note, if the mock is used throughout the test suite, you might want 92 | the implementation to fall back to a stub (or actual) implementation when no 93 | expectations are defined. You can use `stub_with/2` in a case template that 94 | is used throughout your test suite: 95 | 96 | defmodule MyApp.Case do 97 | use ExUnit.CaseTemplate 98 | 99 | setup _ do 100 | Mox.stub_with(MyApp.MockWeatherAPI, MyApp.StubWeatherAPI) 101 | :ok 102 | end 103 | end 104 | 105 | Now, for every test case that uses `ExUnit.Case`, it can use `MyApp.Case` 106 | instead. Then, if no expectations are defined it will call the implementation 107 | in `MyApp.StubWeatherAPI`. 108 | 109 | ## Multiple behaviours 110 | 111 | Mox supports defining mocks for multiple behaviours. 112 | 113 | Suppose your library also defines a behaviour for getting past weather: 114 | 115 | defmodule MyApp.PastWeather do 116 | @callback past_temp(MyApp.LatLong.t(), DateTime.t()) :: {:ok, integer()} 117 | end 118 | 119 | You can mock both the weather and past weather behaviour: 120 | 121 | Mox.defmock(MyApp.MockWeatherAPI, for: [MyApp.Weather, MyApp.PastWeather]) 122 | 123 | ## Compile-time requirements 124 | 125 | If the mock needs to be available during the project compilation, for 126 | instance because you get undefined function warnings, then instead of 127 | defining the mock in your `test_helper.exs`, you should instead define 128 | it under `test/support/mocks.ex`: 129 | 130 | Mox.defmock(MyApp.MockWeatherAPI, for: MyApp.WeatherAPI) 131 | 132 | Then you need to make sure that files in `test/support` get compiled 133 | with the rest of the project. Edit your `mix.exs` file to add the 134 | `test/support` directory to compilation paths: 135 | 136 | def project do 137 | [ 138 | ... 139 | elixirc_paths: elixirc_paths(Mix.env), 140 | ... 141 | ] 142 | end 143 | 144 | defp elixirc_paths(:test), do: ["test/support", "lib"] 145 | defp elixirc_paths(_), do: ["lib"] 146 | 147 | ## Multi-process collaboration 148 | 149 | Mox supports multi-process collaboration via two mechanisms: 150 | 151 | 1. explicit allowances 152 | 2. global mode 153 | 154 | The allowance mechanism can still run tests concurrently while 155 | the global one doesn't. We explore both next. 156 | 157 | ### Explicit allowances 158 | 159 | An allowance permits a child process to use the expectations and stubs 160 | defined in the parent process while still being safe for async tests. 161 | 162 | test "invokes add and mult from a task" do 163 | MyApp.MockWeatherAPI 164 | |> expect(:temp, fn _loc -> {:ok, 30} end) 165 | |> expect(:humidity, fn _loc -> {:ok, 60} end) 166 | 167 | parent_pid = self() 168 | 169 | Task.async(fn -> 170 | MyApp.MockWeatherAPI |> allow(parent_pid, self()) 171 | 172 | assert MyApp.HumanizedWeather.display_temp({50.06, 19.94}) == 173 | "Current temperature is 30 degrees" 174 | 175 | assert MyApp.HumanizedWeather.display_humidity({50.06, 19.94}) == 176 | "Current humidity is 60%" 177 | end) 178 | |> Task.await 179 | end 180 | 181 | Note: if you're running on Elixir 1.8.0 or greater and your concurrency comes 182 | from a `Task` then you don't need to add explicit allowances. Instead 183 | `$callers` is used to determine the process that actually defined the 184 | expectations. 185 | 186 | > #### Parent PIDs {: .tip} 187 | > [Since OTP 25](https://erlang.org/documentation/doc-15.0-rc2/erts-14.3/doc/html/notes.html#erts-13-0), 188 | > `Process.info/2` supports a `:parent` key for retrieving the parent of the given PID. 189 | > Mox started using this in v1.3.0 to determine process tree structures in case `$callers` 190 | > is not available in the process dictionary. This means that even more allowance cases 191 | > are taken care of automatically. 192 | 193 | #### Explicit allowances as lazy/deferred functions 194 | 195 | Under some circumstances, the process might not have been already started 196 | when the allowance happens. In such a case, you might specify the allowance 197 | as a function in the form `(-> pid())`. This function would be resolved late, 198 | at the very moment of dispatch. If the function does not return an existing 199 | PID, Mox will raise a `Mox.UnexpectedCallError` exception. 200 | 201 | ### Global mode 202 | 203 | Mox supports global mode, where any process can consume mocks and stubs 204 | defined in your tests. `set_mox_from_context/0` automatically calls 205 | `set_mox_global/1` but only if the test context **doesn't** include 206 | `async: true`. 207 | 208 | By default the mode is `:private`. 209 | 210 | setup :set_mox_from_context 211 | setup :verify_on_exit! 212 | 213 | test "invokes add and mult from a task" do 214 | MyApp.MockWeatherAPI 215 | |> expect(:temp, fn _loc -> {:ok, 30} end) 216 | |> expect(:humidity, fn _loc -> {:ok, 60} end) 217 | 218 | Task.async(fn -> 219 | assert MyApp.HumanizedWeather.display_temp({50.06, 19.94}) == 220 | "Current temperature is 30 degrees" 221 | 222 | assert MyApp.HumanizedWeather.display_humidity({50.06, 19.94}) == 223 | "Current humidity is 60%" 224 | end) 225 | |> Task.await 226 | end 227 | 228 | ### Blocking on expectations 229 | 230 | If your mock is called in a different process than the test process, 231 | in some cases there is a chance that the test will finish executing 232 | before it has a chance to call the mock and meet the expectations. 233 | Imagine this: 234 | 235 | test "calling a mock from a different process" do 236 | expect(MyApp.MockWeatherAPI, :temp, fn _loc -> {:ok, 30} end) 237 | 238 | spawn(fn -> MyApp.HumanizedWeather.temp({50.06, 19.94}) end) 239 | 240 | verify!() 241 | end 242 | 243 | The test above has a race condition because there is a chance that the 244 | `verify!/0` call will happen before the spawned process calls the mock. 245 | In most cases, you don't control the spawning of the process so you can't 246 | simply monitor the process to know when it dies in order to avoid this 247 | race condition. In those cases, the way to go is to "sync up" with the 248 | process that calls the mock by sending a message to the test process 249 | from the expectation and using that to know when the expectation has been 250 | called. 251 | 252 | test "calling a mock from a different process" do 253 | parent = self() 254 | ref = make_ref() 255 | 256 | expect(MyApp.MockWeatherAPI, :temp, fn _loc -> 257 | send(parent, {ref, :temp}) 258 | {:ok, 30} 259 | end) 260 | 261 | spawn(fn -> MyApp.HumanizedWeather.temp({50.06, 19.94}) end) 262 | 263 | assert_receive {^ref, :temp} 264 | 265 | verify!() 266 | end 267 | 268 | This way, we'll wait until the expectation is called before calling 269 | `verify!/0`. 270 | """ 271 | 272 | @typedoc """ 273 | A mock module. 274 | 275 | This type is available since version 1.1+ of Mox. 276 | """ 277 | @type t() :: module() 278 | 279 | @timeout 30000 280 | @this {:global, Mox.Server} 281 | 282 | defmodule UnexpectedCallError do 283 | defexception [:message] 284 | end 285 | 286 | defmodule VerificationError do 287 | defexception [:message] 288 | end 289 | 290 | @doc """ 291 | Sets the Mox to private mode. 292 | 293 | In private mode, mocks can be set and consumed by the same 294 | process unless other processes are explicitly allowed. 295 | 296 | ## Examples 297 | 298 | setup :set_mox_private 299 | 300 | """ 301 | @spec set_mox_private(term()) :: :ok 302 | def set_mox_private(_context \\ %{}) do 303 | NimbleOwnership.set_mode_to_private(@this) 304 | end 305 | 306 | @doc """ 307 | Sets the Mox to global mode. 308 | 309 | In global mode, mocks can be consumed by any process. 310 | 311 | An ExUnit case where tests use Mox in global mode cannot be 312 | `async: true`. 313 | 314 | ## Examples 315 | 316 | setup :set_mox_global 317 | """ 318 | @spec set_mox_global(term()) :: :ok 319 | def set_mox_global(context \\ %{}) 320 | 321 | def set_mox_global(%{async: true}) do 322 | raise "Mox cannot be set to global mode when the ExUnit case is async. " <> 323 | "If you want to use Mox in global mode, remove \"async: true\" when using ExUnit.Case" 324 | end 325 | 326 | def set_mox_global(_context) do 327 | NimbleOwnership.set_mode_to_shared(@this, self()) 328 | end 329 | 330 | @doc """ 331 | Chooses the Mox mode based on context. 332 | 333 | When `async: true` is used, `set_mox_private/1` is called, 334 | otherwise `set_mox_global/1` is used. 335 | 336 | ## Examples 337 | 338 | setup :set_mox_from_context 339 | 340 | """ 341 | @spec set_mox_from_context(term()) :: :ok 342 | def set_mox_from_context(%{async: true} = _context), do: set_mox_private() 343 | def set_mox_from_context(_context), do: set_mox_global() 344 | 345 | @doc """ 346 | Defines a mock with the given name `:for` the given behaviour(s). 347 | 348 | Mox.defmock(MyMock, for: MyBehaviour) 349 | 350 | With multiple behaviours: 351 | 352 | Mox.defmock(MyMock, for: [MyBehaviour, MyOtherBehaviour]) 353 | 354 | ## Options 355 | 356 | * `:for` - module or list of modules to define the mock module for. 357 | 358 | * `:moduledoc` - `@moduledoc` for the defined mock module. 359 | 360 | * `:skip_optional_callbacks` - boolean to determine whether to skip 361 | or generate optional callbacks in the mock module. 362 | 363 | ## Skipping optional callbacks 364 | 365 | By default, functions are created for all the behaviour's callbacks, 366 | including optional ones. But if for some reason you want to skip one or more 367 | of its `@optional_callbacks`, you can provide the list of callback names to 368 | skip (along with their arities) as `:skip_optional_callbacks`: 369 | 370 | Mox.defmock(MyMock, for: MyBehaviour, skip_optional_callbacks: [on_success: 2]) 371 | 372 | This will define a new mock (`MyMock`) that has a defined function for each 373 | callback on `MyBehaviour` except for `on_success/2`. Note: you can only skip 374 | optional callbacks, not required callbacks. 375 | 376 | You can also pass `true` to skip all optional callbacks, or `false` to keep 377 | the default of generating functions for all optional callbacks. 378 | 379 | ## Passing `@moduledoc` 380 | 381 | You can provide value for `@moduledoc` with `:moduledoc` option. 382 | 383 | Mox.defmock(MyMock, for: MyBehaviour, moduledoc: false) 384 | Mox.defmock(MyMock, for: MyBehaviour, moduledoc: "My mock module.") 385 | 386 | """ 387 | @spec defmock(mock, [option]) :: mock 388 | when mock: t(), 389 | option: 390 | {:for, module() | [module()]} 391 | | {:skip_optional_callbacks, boolean()} 392 | | {:moduledoc, false | String.t()} 393 | def defmock(name, options) when is_atom(name) and is_list(options) do 394 | behaviours = 395 | case Keyword.fetch(options, :for) do 396 | {:ok, mocks} -> List.wrap(mocks) 397 | :error -> raise ArgumentError, ":for option is required on defmock" 398 | end 399 | 400 | skip_optional_callbacks = Keyword.get(options, :skip_optional_callbacks, []) 401 | moduledoc = Keyword.get(options, :moduledoc, false) 402 | 403 | doc_header = generate_doc_header(moduledoc) 404 | compile_header = generate_compile_time_dependency(behaviours) 405 | callbacks_to_skip = validate_skip_optional_callbacks!(behaviours, skip_optional_callbacks) 406 | mock_funs = generate_mock_funs(behaviours, callbacks_to_skip) 407 | 408 | define_mock_module(name, behaviours, doc_header ++ compile_header ++ mock_funs) 409 | 410 | name 411 | end 412 | 413 | defp validate_module!(behaviour) do 414 | ensure_compiled!(behaviour) 415 | end 416 | 417 | defp validate_behaviour!(behaviour) do 418 | if function_exported?(behaviour, :behaviour_info, 1) do 419 | behaviour 420 | else 421 | raise ArgumentError, 422 | "module #{inspect(behaviour)} is not a behaviour, please pass a behaviour to :for" 423 | end 424 | end 425 | 426 | defp generate_doc_header(moduledoc) do 427 | [ 428 | quote do 429 | @moduledoc unquote(moduledoc) 430 | end 431 | ] 432 | end 433 | 434 | defp generate_compile_time_dependency(behaviours) do 435 | for behaviour <- behaviours do 436 | behaviour 437 | |> validate_module!() 438 | |> validate_behaviour!() 439 | 440 | quote do 441 | @behaviour unquote(behaviour) 442 | unquote(behaviour).module_info(:module) 443 | end 444 | end 445 | end 446 | 447 | defp generate_mock_funs(behaviours, callbacks_to_skip) do 448 | for behaviour <- behaviours, 449 | {fun, arity} <- behaviour.behaviour_info(:callbacks), 450 | {fun, arity} not in callbacks_to_skip do 451 | args = 0..arity |> Enum.drop(1) |> Enum.map(&Macro.var(:"arg#{&1}", Elixir)) 452 | 453 | quote do 454 | def unquote(fun)(unquote_splicing(args)) do 455 | Mox.__dispatch__(__MODULE__, unquote(fun), unquote(arity), unquote(args)) 456 | end 457 | end 458 | end 459 | end 460 | 461 | defp validate_skip_optional_callbacks!(behaviours, skip_optional_callbacks) do 462 | all_optional_callbacks = 463 | for behaviour <- behaviours, 464 | {fun, arity} <- behaviour.behaviour_info(:optional_callbacks) do 465 | {fun, arity} 466 | end 467 | 468 | case skip_optional_callbacks do 469 | false -> 470 | [] 471 | 472 | true -> 473 | all_optional_callbacks 474 | 475 | skip_list when is_list(skip_list) -> 476 | for callback <- skip_optional_callbacks, callback not in all_optional_callbacks do 477 | raise ArgumentError, 478 | "all entries in :skip_optional_callbacks must be an optional callback in one " <> 479 | "of the behaviours specified in :for. #{inspect(callback)} was not in the " <> 480 | "list of all optional callbacks: #{inspect(all_optional_callbacks)}" 481 | end 482 | 483 | skip_list 484 | 485 | _ -> 486 | raise ArgumentError, ":skip_optional_callbacks is required to be a list or boolean" 487 | end 488 | end 489 | 490 | defp define_mock_module(name, behaviours, body) do 491 | info = 492 | quote do 493 | def __mock_for__ do 494 | unquote(behaviours) 495 | end 496 | end 497 | 498 | Module.create(name, [info | body], Macro.Env.location(__ENV__)) 499 | end 500 | 501 | @doc """ 502 | Expects the `name` in `mock` with arity given by `code` 503 | to be invoked `n` times. 504 | 505 | If you're calling your mock from an asynchronous process and want 506 | to wait for the mock to be called, see the "Blocking on expectations" 507 | section in the module documentation. 508 | 509 | When `expect/4` is invoked, any previously declared `stub` for the same `name` and arity will 510 | be removed. This ensures that `expect` will fail if the function is called more than `n` times. 511 | If a `stub/3` is invoked **after** `expect/4` for the same `name` and arity, the stub will be 512 | used after all expectations are fulfilled. 513 | 514 | ## Examples 515 | 516 | To expect `MockWeatherAPI.get_temp/1` to be called once: 517 | 518 | expect(MockWeatherAPI, :get_temp, fn _ -> {:ok, 30} end) 519 | 520 | To expect `MockWeatherAPI.get_temp/1` to be called five times: 521 | 522 | expect(MockWeatherAPI, :get_temp, 5, fn _ -> {:ok, 30} end) 523 | 524 | To expect `MockWeatherAPI.get_temp/1` not to be called (see also `deny/3`): 525 | 526 | expect(MockWeatherAPI, :get_temp, 0, fn _ -> {:ok, 30} end) 527 | 528 | `expect/4` can also be invoked multiple times for the same name/arity, 529 | allowing you to give different behaviours on each invocation. For instance, 530 | you could test that your code will try an API call three times before giving 531 | up: 532 | 533 | MockWeatherAPI 534 | |> expect(:get_temp, 2, fn _loc -> {:error, :unreachable} end) 535 | |> expect(:get_temp, 1, fn _loc -> {:ok, 30} end) 536 | 537 | log = capture_log(fn -> 538 | assert Weather.current_temp(location) 539 | == "It's currently 30 degrees" 540 | end) 541 | 542 | assert log =~ "attempt 1 failed" 543 | assert log =~ "attempt 2 failed" 544 | assert log =~ "attempt 3 succeeded" 545 | 546 | MockWeatherAPI 547 | |> expect(:get_temp, 3, fn _loc -> {:error, :unreachable} end) 548 | 549 | assert Weather.current_temp(location) == "Current temperature is unavailable" 550 | """ 551 | @spec expect(mock, atom(), non_neg_integer(), function()) :: mock when mock: t() 552 | def expect(mock, name, n \\ 1, code) 553 | when is_atom(mock) and is_atom(name) and is_integer(n) and n >= 0 and is_function(code) do 554 | calls = List.duplicate(code, n) 555 | arity = arity(code) 556 | add_expectation!(mock, name, arity, {n, calls, nil}) 557 | mock 558 | end 559 | 560 | @doc """ 561 | Ensures that `name`/`arity` in `mock` is not invoked. 562 | 563 | When `deny/3` is invoked, any previously declared `stub` for the same `name` and arity will 564 | be removed. This ensures that `deny` will fail if the function is called. If a `stub/3` is 565 | invoked **after** `deny/3` for the same `name` and `arity`, the stub will be used instead, so 566 | `deny` will have no effect. 567 | 568 | ## Examples 569 | 570 | To expect `MockWeatherAPI.get_temp/1` to never be called: 571 | 572 | deny(MockWeatherAPI, :get_temp, 1) 573 | 574 | """ 575 | @doc since: "1.2.0" 576 | @spec deny(mock, atom(), non_neg_integer()) :: mock when mock: t() 577 | def deny(mock, name, arity) 578 | when is_atom(mock) and is_atom(name) and is_integer(arity) and arity >= 0 do 579 | add_expectation!(mock, name, arity, {0, [], nil}) 580 | mock 581 | end 582 | 583 | @doc """ 584 | Allows the `name` in `mock` with arity given by `code` to 585 | be invoked zero or many times. 586 | 587 | Unlike expectations, stubs are never verified. 588 | 589 | If expectations and stubs are defined for the same function 590 | and arity, the stub is invoked only after all expectations are 591 | fulfilled. 592 | 593 | ## Examples 594 | 595 | To allow `MockWeatherAPI.get_temp/1` to be called any number of times: 596 | 597 | stub(MockWeatherAPI, :get_temp, fn _loc -> {:ok, 30} end) 598 | 599 | `stub/3` will overwrite any previous calls to `stub/3`. 600 | """ 601 | @spec stub(mock, atom(), function()) :: mock when mock: t() 602 | def stub(mock, name, code) 603 | when is_atom(mock) and is_atom(name) and is_function(code) do 604 | arity = arity(code) 605 | add_expectation!(mock, name, arity, {0, [], code}) 606 | mock 607 | end 608 | 609 | @doc """ 610 | Stubs all functions described by the shared behaviours in the `mock` and `module`. 611 | 612 | ## Examples 613 | 614 | defmodule MyApp.WeatherAPI do 615 | @callback temp(MyApp.LatLong.t()) :: {:ok, integer()} 616 | @callback humidity(MyApp.LatLong.t()) :: {:ok, integer()} 617 | end 618 | 619 | defmodule MyApp.StubWeatherAPI do 620 | @behaviour MyApp.WeatherAPI 621 | def temp(_loc), do: {:ok, 30} 622 | def humidity(_loc), do: {:ok, 60} 623 | end 624 | 625 | defmock(MyApp.MockWeatherAPI, for: MyApp.WeatherAPI) 626 | 627 | setup do 628 | stub_with(MyApp.MockWeatherAPI, MyApp.StubWeatherAPI) 629 | :ok 630 | end 631 | 632 | This is the same as calling `stub/3` for each callback in `MyApp.MockWeatherAPI`: 633 | 634 | stub(MyApp.MockWeatherAPI, :temp, &MyApp.StubWeatherAPI.temp/1) 635 | stub(MyApp.MockWeatherAPI, :humidity, &MyApp.StubWeatherAPI.humidity/1) 636 | 637 | """ 638 | @spec stub_with(mock, module()) :: mock when mock: t() 639 | def stub_with(mock, module) when is_atom(mock) and is_atom(module) do 640 | behaviours = module_behaviours(module) 641 | behaviours_mock = mock.__mock_for__() 642 | behaviours_common = Enum.filter(behaviours, &(&1 in behaviours_mock)) 643 | 644 | do_stub_with(mock, module, behaviours, behaviours_common) 645 | end 646 | 647 | defp do_stub_with(_mock, module, [], _behaviours_common) do 648 | raise ArgumentError, "#{inspect(module)} does not implement any behaviour" 649 | end 650 | 651 | defp do_stub_with(mock, module, _behaviours, []) do 652 | raise ArgumentError, 653 | "#{inspect(module)} and #{inspect(mock)} do not share any behaviour" 654 | end 655 | 656 | defp do_stub_with(mock, module, behaviours, _behaviours_common) do 657 | key_expectation_list = 658 | for behaviour <- behaviours, 659 | {fun, arity} <- behaviour.behaviour_info(:callbacks), 660 | function_exported?(mock, fun, arity) do 661 | { 662 | {mock, fun, arity}, 663 | {0, [], :erlang.make_fun(module, fun, arity)} 664 | } 665 | end 666 | 667 | add_expectations!(mock, key_expectation_list) 668 | 669 | mock 670 | end 671 | 672 | defp module_behaviours(module) do 673 | module.module_info(:attributes) 674 | |> Keyword.get_values(:behaviour) 675 | |> List.flatten() 676 | end 677 | 678 | defp arity(code) do 679 | {:arity, arity} = :erlang.fun_info(code, :arity) 680 | arity 681 | end 682 | 683 | defp add_expectation!(mock, name, arity, value) do 684 | validate_mock!(mock) 685 | key = {mock, name, arity} 686 | 687 | unless function_exported?(mock, name, arity) do 688 | raise ArgumentError, "unknown function #{name}/#{arity} for mock #{inspect(mock)}" 689 | end 690 | 691 | case add_expectations(self(), mock, [{key, value}]) do 692 | :ok -> 693 | :ok 694 | 695 | {:error, error} -> 696 | raise_cannot_add_expectation!(error, mock) 697 | end 698 | end 699 | 700 | defp add_expectations!(mock, key_expectation_list) do 701 | validate_mock!(mock) 702 | 703 | case add_expectations(self(), mock, key_expectation_list) do 704 | :ok -> 705 | :ok 706 | 707 | {:error, error} -> 708 | raise_cannot_add_expectation!(error, mock) 709 | end 710 | end 711 | 712 | defp raise_cannot_add_expectation!( 713 | %NimbleOwnership.Error{reason: {:already_allowed, owner_pid}}, 714 | mock 715 | ) do 716 | inspected = inspect(self()) 717 | 718 | raise ArgumentError, """ 719 | cannot add expectations/stubs to #{inspect(mock)} in the current process (#{inspected}) \ 720 | because the process has been allowed by #{inspect(owner_pid)}. \ 721 | You cannot define expectations/stubs in a process that has been allowed 722 | """ 723 | end 724 | 725 | defp raise_cannot_add_expectation!( 726 | %NimbleOwnership.Error{reason: {:not_shared_owner, global_pid}}, 727 | mock 728 | ) do 729 | inspected = inspect(self()) 730 | 731 | raise ArgumentError, """ 732 | cannot add expectations/stubs to #{inspect(mock)} in the current process (#{inspected}) \ 733 | because Mox is in global mode and the global process is #{inspect(global_pid)}. \ 734 | Only the process that set Mox to global can set expectations/stubs in global mode 735 | """ 736 | end 737 | 738 | defp raise_cannot_add_expectation!(error, _mock) do 739 | raise error 740 | end 741 | 742 | @doc """ 743 | Allows other processes to share expectations and stubs 744 | defined by owner process. 745 | 746 | ## Examples 747 | 748 | To allow `child_pid` to call any stubs or expectations defined for `MyMock`: 749 | 750 | allow(MyMock, self(), child_pid) 751 | 752 | `allow/3` also accepts named process or via references: 753 | 754 | allow(MyMock, self(), SomeChildProcess) 755 | 756 | If the process is not yet started at the moment of allowance definition, 757 | it might be allowed as a function, assuming at the moment of invocation 758 | it would have been started. If the function cannot be resolved to a `pid` 759 | during invocation, the expectation will not succeed. 760 | 761 | allow(MyMock, self(), fn -> GenServer.whereis(Deferred) end) 762 | 763 | """ 764 | @spec allow(mock, pid(), term()) :: mock when mock: t() 765 | def allow(mock, owner_pid, allowed_via) when is_atom(mock) and is_pid(owner_pid) do 766 | allowed_pid_or_function = 767 | case allowed_via do 768 | fun when is_function(fun, 0) -> fun 769 | pid_or_name -> GenServer.whereis(pid_or_name) 770 | end 771 | 772 | if allowed_pid_or_function == owner_pid do 773 | raise ArgumentError, "owner_pid and allowed_pid must be different" 774 | end 775 | 776 | case NimbleOwnership.allow(@this, owner_pid, allowed_pid_or_function, mock, @timeout) do 777 | :ok -> 778 | mock 779 | 780 | {:error, %NimbleOwnership.Error{reason: :not_allowed}} -> 781 | # Init the mock and re-allow. 782 | _ = get_and_update!(owner_pid, mock, &{&1, %{}}) 783 | allow(mock, owner_pid, allowed_via) 784 | mock 785 | 786 | {:error, %NimbleOwnership.Error{reason: {:already_allowed, actual_pid}}} -> 787 | raise ArgumentError, """ 788 | cannot allow #{inspect(allowed_pid_or_function)} to use #{inspect(mock)} \ 789 | from #{inspect(owner_pid)} \ 790 | because it is already allowed by #{inspect(actual_pid)}. 791 | 792 | If you are seeing this error message, it is because you are either \ 793 | setting up allowances from different processes or your tests have \ 794 | async: true and you found a race condition where two different tests \ 795 | are allowing the same process 796 | """ 797 | 798 | {:error, %NimbleOwnership.Error{reason: :already_an_owner}} -> 799 | raise ArgumentError, """ 800 | cannot allow #{inspect(allowed_pid_or_function)} to use \ 801 | #{inspect(mock)} from #{inspect(owner_pid)} \ 802 | because the process has already defined its own expectations/stubs 803 | """ 804 | 805 | {:error, %NimbleOwnership.Error{reason: :cant_allow_in_shared_mode}} -> 806 | # Already allowed 807 | mock 808 | end 809 | end 810 | 811 | @doc """ 812 | Verifies the current process after it exits. 813 | 814 | If you want to verify expectations for all tests, you can use 815 | `verify_on_exit!/1` as a setup callback: 816 | 817 | setup :verify_on_exit! 818 | 819 | """ 820 | @spec verify_on_exit!(term()) :: :ok 821 | def verify_on_exit!(_context \\ %{}) do 822 | pid = self() 823 | NimbleOwnership.set_owner_to_manual_cleanup(@this, pid) 824 | 825 | ExUnit.Callbacks.on_exit(Mox, fn -> 826 | __verify_mock_or_all__(pid, :all) 827 | NimbleOwnership.cleanup_owner(@this, pid) 828 | end) 829 | end 830 | 831 | @doc """ 832 | Verifies that all expectations set by the current process 833 | have been called. 834 | """ 835 | @spec verify!() :: :ok 836 | def verify! do 837 | __verify_mock_or_all__(self(), :all) 838 | end 839 | 840 | @doc """ 841 | Verifies that all expectations in `mock` have been called. 842 | """ 843 | @spec verify!(t()) :: :ok 844 | def verify!(mock) do 845 | validate_mock!(mock) 846 | __verify_mock_or_all__(self(), mock) 847 | end 848 | 849 | # Made public for testing. 850 | @doc false 851 | def __verify_mock_or_all__(owner_pid, mock_or_all) do 852 | all_expectations = NimbleOwnership.get_owned(@this, owner_pid, _default = %{}, @timeout) 853 | 854 | pending = 855 | for {_mock, expected_funs} <- all_expectations, 856 | {{module, _, _} = key, {count, [_ | _] = calls, _stub}} <- expected_funs, 857 | module == mock_or_all or mock_or_all == :all do 858 | {key, count, length(calls)} 859 | end 860 | 861 | messages = 862 | for {{module, name, arity}, total, pending} <- pending do 863 | mfa = Exception.format_mfa(module, name, arity) 864 | called = total - pending 865 | " * expected #{mfa} to be invoked #{times(total)} but it was invoked #{times(called)}" 866 | end 867 | 868 | if messages != [] do 869 | raise VerificationError, 870 | "error while verifying mocks for #{inspect(owner_pid)}:\n\n" <> 871 | Enum.join(messages, "\n") 872 | end 873 | 874 | :ok 875 | end 876 | 877 | defp validate_mock!(mock) do 878 | ensure_compiled!(mock) 879 | 880 | unless function_exported?(mock, :__mock_for__, 0) do 881 | raise ArgumentError, "module #{inspect(mock)} is not a mock" 882 | end 883 | 884 | :ok 885 | end 886 | 887 | @compile {:no_warn_undefined, {Code, :ensure_compiled!, 1}} 888 | 889 | defp ensure_compiled!(mod) do 890 | if function_exported?(Code, :ensure_compiled!, 1) do 891 | Code.ensure_compiled!(mod) 892 | else 893 | case Code.ensure_compiled(mod) do 894 | {:module, mod} -> 895 | mod 896 | 897 | {:error, reason} -> 898 | raise ArgumentError, 899 | "could not load module #{inspect(mod)} due to reason #{inspect(reason)}" 900 | end 901 | end 902 | end 903 | 904 | @doc false 905 | def __dispatch__(mock, name, arity, args) do 906 | case fetch_fun_to_dispatch([self() | caller_pids()], {mock, name, arity}) do 907 | :no_expectation -> 908 | mfa = Exception.format_mfa(mock, name, arity) 909 | 910 | raise UnexpectedCallError, 911 | "no expectation defined for #{mfa} in #{format_process()} with args #{inspect(args)}" 912 | 913 | {:out_of_expectations, count} -> 914 | mfa = Exception.format_mfa(mock, name, arity) 915 | 916 | raise UnexpectedCallError, 917 | "expected #{mfa} to be called #{times(count)} but it has been " <> 918 | "called #{times(count + 1)} in #{format_process()}" 919 | 920 | {:remote, fun_to_call} -> 921 | # It's possible that Mox.Server is running on a remote node in the cluster. Since the 922 | # function that we passed is not guaranteed to exist on that node (it might have come 923 | # from a .exs file), find the remote node that hosts Mox.Server, and run the function 924 | # on that node. 925 | Mox.Server 926 | |> :global.whereis_name() 927 | |> node() 928 | |> :rpc.call(Kernel, :apply, [fun_to_call, args]) 929 | 930 | {:ok, fun_to_call} -> 931 | apply(fun_to_call, args) 932 | end 933 | end 934 | 935 | defp times(1), do: "once" 936 | defp times(n), do: "#{n} times" 937 | 938 | defp format_process do 939 | callers = caller_pids() 940 | 941 | "process #{inspect(self())}" <> 942 | if Enum.empty?(callers) do 943 | "" 944 | else 945 | " (or in its callers #{inspect(callers)})" 946 | end 947 | end 948 | 949 | # Find the pid of the actual caller 950 | defp caller_pids do 951 | case Process.get(:"$callers") do 952 | nil -> [self() | recursive_parents(self())] 953 | pids when is_list(pids) -> pids 954 | end 955 | end 956 | 957 | # A PID with no parent has :undefined as its parent. 958 | defp recursive_parents(:undefined) do 959 | [] 960 | end 961 | 962 | defp recursive_parents(pid) when is_pid(pid) do 963 | Process.info(pid, :parent) 964 | rescue 965 | # erlang:process_info(Pid, parent) is not available, as it was released in 966 | # ERTS 13.0 (https://erlang.org/documentation/doc-15.0-rc2/erts-14.3/doc/html/notes.html#erts-13-0) 967 | ArgumentError -> 968 | [] 969 | else 970 | {:parent, parent_pid} -> 971 | [parent_pid | recursive_parents(parent_pid)] 972 | end 973 | 974 | ## Ownership 975 | 976 | @doc false 977 | def start_link_ownership do 978 | case NimbleOwnership.start_link(name: @this) do 979 | {:error, {:already_started, _}} -> :ignore 980 | other -> other 981 | end 982 | end 983 | 984 | defp add_expectations(owner_pid, mock, key_expectation_list) do 985 | update_fun = &{:ok, init_or_merge_expectations(&1, key_expectation_list)} 986 | 987 | case get_and_update(owner_pid, mock, update_fun) do 988 | {:ok, _value} -> 989 | :ok 990 | 991 | {:error, error} -> 992 | {:error, error} 993 | end 994 | end 995 | 996 | defp fetch_fun_to_dispatch(caller_pids, {mock, _, _} = key) do 997 | parent = self() 998 | 999 | with {:ok, owner_pid} <- fetch_owner_from_callers(caller_pids, mock) do 1000 | get_and_update!(owner_pid, mock, fn expectations -> 1001 | case expectations[key] do 1002 | nil -> 1003 | {:no_expectation, expectations} 1004 | 1005 | {total, [], nil} -> 1006 | {{:out_of_expectations, total}, expectations} 1007 | 1008 | {_, [], stub} -> 1009 | {{ok_or_remote(parent), stub}, expectations} 1010 | 1011 | {total, [call | calls], stub} -> 1012 | new_expectations = put_in(expectations[key], {total, calls, stub}) 1013 | {{ok_or_remote(parent), call}, new_expectations} 1014 | end 1015 | end) 1016 | end 1017 | end 1018 | 1019 | defp fetch_owner_from_callers(caller_pids, mock) do 1020 | # If the mock doesn't have an owner, it can't have expectations so we return :no_expectation. 1021 | case NimbleOwnership.fetch_owner(@this, caller_pids, mock, @timeout) do 1022 | {tag, owner_pid} when tag in [:shared_owner, :ok] -> {:ok, owner_pid} 1023 | :error -> :no_expectation 1024 | end 1025 | end 1026 | 1027 | defp get_and_update(owner_pid, mock, update_fun) do 1028 | NimbleOwnership.get_and_update(@this, owner_pid, mock, update_fun, @timeout) 1029 | end 1030 | 1031 | defp get_and_update!(owner_pid, mock, update_fun) do 1032 | case get_and_update(owner_pid, mock, update_fun) do 1033 | {:ok, return} -> return 1034 | {:error, %NimbleOwnership.Error{} = error} -> raise error 1035 | end 1036 | end 1037 | 1038 | defp init_or_merge_expectations(current_exps, [{key, {n, calls, stub} = new_exp} | tail]) 1039 | when is_map(current_exps) or is_nil(current_exps) do 1040 | init_or_merge_expectations( 1041 | Map.update(current_exps || %{}, key, new_exp, fn {current_n, current_calls, _current_stub} -> 1042 | {current_n + n, current_calls ++ calls, stub} 1043 | end), 1044 | tail 1045 | ) 1046 | end 1047 | 1048 | defp init_or_merge_expectations(current_exps, []), do: current_exps 1049 | 1050 | defp ok_or_remote(source) when node(source) == node(), do: :ok 1051 | defp ok_or_remote(_source), do: :remote 1052 | end 1053 | -------------------------------------------------------------------------------- /lib/mox/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Mox.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | def start(_, _) do 7 | children = [ 8 | %{id: Mox, type: :worker, start: {Mox, :start_link_ownership, []}} 9 | ] 10 | 11 | Supervisor.start_link(children, name: Mox.Supervisor, strategy: :one_for_one) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Mox.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.2.0" 5 | 6 | def project do 7 | [ 8 | app: :mox, 9 | version: @version, 10 | elixir: "~> 1.11", 11 | elixirc_paths: elixirc_paths(Mix.env()), 12 | start_permanent: Mix.env() == :prod, 13 | name: "Mox", 14 | description: "Mocks and explicit contracts for Elixir", 15 | deps: deps(), 16 | docs: docs(), 17 | package: package(), 18 | test_coverage: [tool: ExCoveralls], 19 | preferred_cli_env: [ 20 | coveralls: :test, 21 | "coveralls.detail": :test, 22 | "coveralls.post": :test, 23 | "coveralls.html": :test 24 | ] 25 | ] 26 | end 27 | 28 | def application do 29 | [ 30 | extra_applications: [:logger], 31 | mod: {Mox.Application, []} 32 | ] 33 | end 34 | 35 | defp elixirc_paths(:test), do: ["test/support", "lib"] 36 | defp elixirc_paths(_), do: ["lib"] 37 | 38 | defp deps do 39 | [ 40 | {:nimble_ownership, "~> 1.0"}, 41 | {:castore, "~> 1.0", only: :test}, 42 | {:ex_doc, "~> 0.16", only: :docs}, 43 | {:excoveralls, "~> 0.18", only: :test} 44 | ] 45 | end 46 | 47 | defp docs do 48 | [ 49 | main: "Mox", 50 | source_ref: "v#{@version}", 51 | source_url: "https://github.com/dashbitco/mox" 52 | ] 53 | end 54 | 55 | defp package do 56 | %{ 57 | licenses: ["Apache-2.0"], 58 | maintainers: ["José Valim"], 59 | links: %{"GitHub" => "https://github.com/dashbitco/mox"} 60 | } 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, 4 | "ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"}, 5 | "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, 6 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 7 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 9 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 10 | "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, 11 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 12 | } 13 | -------------------------------------------------------------------------------- /test/cluster_test.exs: -------------------------------------------------------------------------------- 1 | # :peer module only available in otp release 25 or greater 2 | 3 | otp_release = 4 | :otp_release 5 | |> :erlang.system_info() 6 | |> List.to_integer() 7 | 8 | if otp_release >= 25 do 9 | defmodule MoxTest.ClusterTest do 10 | use ExUnit.Case 11 | import Mox 12 | 13 | # These tests don't work if code coverage is enabled. 14 | @moduletag :fails_on_coverage 15 | 16 | setup_all do 17 | Task.start_link(fn -> 18 | System.cmd("epmd", []) 19 | end) 20 | 21 | {:ok, _} = :net_kernel.start([:primary, :shortnames]) 22 | :peer.start(%{name: :peer}) 23 | [peer] = Node.list() 24 | :rpc.call(peer, :code, :add_paths, [:code.get_path()]) 25 | :rpc.call(peer, Application, :ensure_all_started, [:mix]) 26 | :rpc.call(peer, Application, :ensure_all_started, [:logger]) 27 | :rpc.call(peer, Logger, :configure, [[level: Logger.level()]]) 28 | :rpc.call(peer, Mix, :env, [Mix.env()]) 29 | # force the primary global Mox Server registration to be synced, otherwise 30 | # there is a race condition, since global doesn't actively sync all the time. 31 | # the sync event should happen BEFORE connecting mox, otherwise it is not 32 | # deterministic which global mox server will survive. 33 | :rpc.call(peer, :global, :sync, []) 34 | :rpc.call(peer, Application, :ensure_all_started, [:mox]) 35 | :ok 36 | end 37 | 38 | test "an allowance can commute over the cluster in private mode" do 39 | set_mox_private() 40 | 41 | Mox.expect(CalcMock, :add, fn a, b -> a + b end) 42 | 43 | quoted = 44 | quote do 45 | primary = 46 | receive do 47 | {:unblock, pid} -> pid 48 | end 49 | 50 | send(primary, {:result, CalcMock.add(1, 1)}) 51 | end 52 | 53 | peer = 54 | Node.list() 55 | |> List.first() 56 | |> Node.spawn(Code, :eval_quoted, [quoted]) 57 | 58 | Mox.allow(CalcMock, self(), peer) 59 | send(peer, {:unblock, self()}) 60 | assert_receive {:result, 2} 61 | end 62 | 63 | test "an allowance can commute over the cluster in global mode" do 64 | set_mox_global() 65 | 66 | Mox.expect(CalcMock, :add, fn a, b -> a + b end) 67 | 68 | quoted = 69 | quote do 70 | primary = 71 | receive do 72 | {:unblock, pid} -> pid 73 | end 74 | 75 | send(primary, {:result, CalcMock.add(1, 1)}) 76 | end 77 | 78 | peer = 79 | Node.list() 80 | |> List.first() 81 | |> Node.spawn(Code, :eval_quoted, [quoted]) 82 | 83 | send(peer, {:unblock, self()}) 84 | assert_receive {:result, 2} 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/mox_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MoxTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mox 5 | doctest Mox 6 | 7 | @compile {:no_warn_undefined, 8 | [ 9 | MyTrueMock, 10 | MyFalseMock, 11 | OneBehaviourMock, 12 | MyMultiMock, 13 | MultiBehaviourMock, 14 | MyScientificMock, 15 | SciCalcMockWithoutOptional 16 | ]} 17 | 18 | def in_all_modes(callback) do 19 | set_mox_global() 20 | callback.() 21 | set_mox_private() 22 | callback.() 23 | end 24 | 25 | describe "defmock/2" do 26 | test "raises for unknown module" do 27 | assert_raise ArgumentError, fn -> 28 | defmock(MyMock, for: Unknown) 29 | end 30 | end 31 | 32 | test "raises for non behaviour" do 33 | assert_raise ArgumentError, ~r"module String is not a behaviour", fn -> 34 | defmock(MyMock, for: String) 35 | end 36 | end 37 | 38 | test "raises if :for is missing" do 39 | assert_raise ArgumentError, ":for option is required on defmock", fn -> 40 | defmock(MyMock, []) 41 | end 42 | end 43 | 44 | test "accepts a list of behaviours" do 45 | assert defmock(MyMock, for: [Calculator, ScientificCalculator]) 46 | end 47 | 48 | test "defines a mock function for all callbacks by default" do 49 | defmock(MyScientificMock, for: ScientificCalculator) 50 | all_callbacks = ScientificCalculator.behaviour_info(:callbacks) 51 | assert all_callbacks -- MyScientificMock.__info__(:functions) == [] 52 | end 53 | 54 | test "accepts a list of callbacks to skip" do 55 | defmock(MyMultiMock, 56 | for: [Calculator, ScientificCalculator], 57 | skip_optional_callbacks: [sin: 1] 58 | ) 59 | 60 | all_callbacks = ScientificCalculator.behaviour_info(:callbacks) 61 | assert all_callbacks -- MyMultiMock.__info__(:functions) == [sin: 1] 62 | end 63 | 64 | test "accepts false to indicate all functions should be generated" do 65 | defmock(MyFalseMock, 66 | for: [Calculator, ScientificCalculator], 67 | skip_optional_callbacks: false 68 | ) 69 | 70 | all_callbacks = ScientificCalculator.behaviour_info(:callbacks) 71 | assert all_callbacks -- MyFalseMock.__info__(:functions) == [] 72 | end 73 | 74 | test "accepts true to indicate no optional functions should be generated" do 75 | defmock(MyTrueMock, for: [Calculator, ScientificCalculator], skip_optional_callbacks: true) 76 | all_callbacks = ScientificCalculator.behaviour_info(:callbacks) 77 | assert all_callbacks -- MyTrueMock.__info__(:functions) == [sin: 1] 78 | end 79 | 80 | test "raises if :skip_optional_callbacks is not a list or boolean" do 81 | assert_raise ArgumentError, 82 | ":skip_optional_callbacks is required to be a list or boolean", 83 | fn -> 84 | defmock(MyMock, for: Calculator, skip_optional_callbacks: 42) 85 | end 86 | end 87 | 88 | test "raises if a callback in :skip_optional_callbacks does not exist" do 89 | expected_error = 90 | "all entries in :skip_optional_callbacks must be an optional callback in" <> 91 | " one of the behaviours specified in :for. {:some_other_function, 0} was not in the list" <> 92 | " of all optional callbacks: []" 93 | 94 | assert_raise ArgumentError, expected_error, fn -> 95 | defmock(MyMock, 96 | for: Calculator, 97 | skip_optional_callbacks: [some_other_function: 0] 98 | ) 99 | end 100 | end 101 | 102 | test "raises if a callback in :skip_optional_callbacks is not an optional callback" do 103 | expected_error = 104 | "all entries in :skip_optional_callbacks must be an optional callback in" <> 105 | " one of the behaviours specified in :for. {:exponent, 2} was not in the list" <> 106 | " of all optional callbacks: [sin: 1]" 107 | 108 | assert_raise ArgumentError, expected_error, fn -> 109 | defmock(MyMock, 110 | for: ScientificCalculator, 111 | skip_optional_callbacks: [exponent: 2] 112 | ) 113 | end 114 | end 115 | 116 | @tag :requires_code_fetch_docs 117 | test "uses false for when moduledoc is not given" do 118 | assert {:docs_v1, _, :elixir, "text/markdown", :hidden, _, _} = 119 | Code.fetch_docs(MyMockWithoutModuledoc) 120 | end 121 | 122 | @tag :requires_code_fetch_docs 123 | test "passes value to @moduledoc if moduledoc is given" do 124 | assert {:docs_v1, _, :elixir, "text/markdown", :hidden, _, _} = 125 | Code.fetch_docs(MyMockWithFalseModuledoc) 126 | 127 | assert {:docs_v1, _, :elixir, "text/markdown", %{"en" => "hello world"}, _, _} = 128 | Code.fetch_docs(MyMockWithStringModuledoc) 129 | end 130 | 131 | test "has behaviours of what it mocks" do 132 | defmock(OneBehaviourMock, for: Calculator) 133 | assert one_behaviour = OneBehaviourMock.__info__(:attributes) 134 | assert {:behaviour, [Calculator]} in one_behaviour 135 | 136 | defmock(MultiBehaviourMock, for: [Calculator, ScientificCalculator]) 137 | assert two_behaviour = MultiBehaviourMock.__info__(:attributes) 138 | assert {:behaviour, [Calculator]} in two_behaviour 139 | assert {:behaviour, [ScientificCalculator]} in two_behaviour 140 | end 141 | end 142 | 143 | describe "expect/4" do 144 | test "works with multiple behaviours" do 145 | SciCalcMock 146 | |> expect(:exponent, fn x, y -> :math.pow(x, y) end) 147 | |> expect(:add, fn x, y -> x + y end) 148 | 149 | assert SciCalcMock.exponent(2, 3) == 8 150 | assert SciCalcMock.add(2, 3) == 5 151 | end 152 | 153 | test "is invoked n times by the same process in private mode" do 154 | set_mox_private() 155 | 156 | CalcMock 157 | |> expect(:add, 2, fn x, y -> x + y end) 158 | |> expect(:mult, fn x, y -> x * y end) 159 | |> expect(:add, fn _, _ -> 0 end) 160 | 161 | assert CalcMock.add(2, 3) == 5 162 | assert CalcMock.add(3, 2) == 5 163 | assert CalcMock.add(:whatever, :whatever) == 0 164 | assert CalcMock.mult(3, 2) == 6 165 | end 166 | 167 | test "is invoked n times by any process in global mode" do 168 | set_mox_global() 169 | 170 | CalcMock 171 | |> expect(:add, 2, fn x, y -> x + y end) 172 | |> expect(:mult, fn x, y -> x * y end) 173 | |> expect(:add, fn _, _ -> 0 end) 174 | 175 | task = 176 | Task.async(fn -> 177 | assert CalcMock.add(2, 3) == 5 178 | assert CalcMock.add(3, 2) == 5 179 | end) 180 | 181 | Task.await(task) 182 | 183 | assert CalcMock.add(:whatever, :whatever) == 0 184 | assert CalcMock.mult(3, 2) == 6 185 | end 186 | 187 | @tag :requires_caller_tracking 188 | test "is invoked n times by any process in private mode on Elixir 1.8" do 189 | set_mox_private() 190 | 191 | CalcMock 192 | |> expect(:add, 2, fn x, y -> x + y end) 193 | |> expect(:mult, fn x, y -> x * y end) 194 | |> expect(:add, fn _, _ -> 0 end) 195 | 196 | task = 197 | Task.async(fn -> 198 | assert CalcMock.add(2, 3) == 5 199 | assert CalcMock.add(3, 2) == 5 200 | end) 201 | 202 | Task.await(task) 203 | 204 | assert CalcMock.add(:whatever, :whatever) == 0 205 | assert CalcMock.mult(3, 2) == 6 206 | end 207 | 208 | @tag :requires_caller_tracking 209 | test "is invoked n times by a sub-process in private mode on Elixir 1.8" do 210 | set_mox_private() 211 | 212 | CalcMock 213 | |> expect(:add, 2, fn x, y -> x + y end) 214 | |> expect(:mult, fn x, y -> x * y end) 215 | |> expect(:add, fn _, _ -> 0 end) 216 | 217 | task = 218 | Task.async(fn -> 219 | assert CalcMock.add(2, 3) == 5 220 | assert CalcMock.add(3, 2) == 5 221 | 222 | inner_task = 223 | Task.async(fn -> 224 | assert CalcMock.add(:whatever, :whatever) == 0 225 | assert CalcMock.mult(3, 2) == 6 226 | end) 227 | 228 | Task.await(inner_task) 229 | end) 230 | 231 | Task.await(task) 232 | end 233 | 234 | test "allows asserting that function is not called" do 235 | CalcMock 236 | |> expect(:add, 0, fn x, y -> x + y end) 237 | 238 | msg = ~r"expected CalcMock.add/2 to be called 0 times but it has been called once" 239 | 240 | assert_raise Mox.UnexpectedCallError, msg, fn -> 241 | CalcMock.add(2, 3) == 5 242 | end 243 | end 244 | 245 | test "can be recharged" do 246 | expect(CalcMock, :add, fn x, y -> x + y end) 247 | assert CalcMock.add(2, 3) == 5 248 | 249 | expect(CalcMock, :add, fn x, y -> x + y end) 250 | assert CalcMock.add(3, 2) == 5 251 | end 252 | 253 | test "expectations are reclaimed if the global process dies" do 254 | task = 255 | Task.async(fn -> 256 | set_mox_global() 257 | 258 | CalcMock 259 | |> expect(:add, fn _, _ -> :expected end) 260 | |> stub(:mult, fn _, _ -> :stubbed end) 261 | end) 262 | 263 | Task.await(task) 264 | 265 | assert_raise Mox.UnexpectedCallError, fn -> 266 | CalcMock.add(1, 1) 267 | end 268 | 269 | CalcMock 270 | |> expect(:add, 1, fn x, y -> x + y end) 271 | 272 | assert CalcMock.add(1, 1) == 2 273 | end 274 | 275 | test "raises if a non-mock is given" do 276 | assert_raise ArgumentError, ~r"could not load module Unknown", fn -> 277 | expect(Unknown, :add, fn x, y -> x + y end) 278 | end 279 | 280 | assert_raise ArgumentError, ~r"module String is not a mock", fn -> 281 | expect(String, :add, fn x, y -> x + y end) 282 | end 283 | end 284 | 285 | test "raises if function is not in behaviour" do 286 | assert_raise ArgumentError, ~r"unknown function oops/2 for mock CalcMock", fn -> 287 | expect(CalcMock, :oops, fn x, y -> x + y end) 288 | end 289 | 290 | assert_raise ArgumentError, ~r"unknown function add/3 for mock CalcMock", fn -> 291 | expect(CalcMock, :add, fn x, y, z -> x + y + z end) 292 | end 293 | end 294 | 295 | test "raises if there is no expectation" do 296 | assert_raise Mox.UnexpectedCallError, 297 | ~r"no expectation defined for CalcMock\.add/2.*with args \[2, 3\]", 298 | fn -> 299 | CalcMock.add(2, 3) == 5 300 | end 301 | end 302 | 303 | test "raises if all expectations are consumed" do 304 | expect(CalcMock, :add, fn x, y -> x + y end) 305 | assert CalcMock.add(2, 3) == 5 306 | 307 | assert_raise Mox.UnexpectedCallError, ~r"expected CalcMock.add/2 to be called once", fn -> 308 | CalcMock.add(2, 3) == 5 309 | end 310 | 311 | expect(CalcMock, :add, fn x, y -> x + y end) 312 | assert CalcMock.add(2, 3) == 5 313 | 314 | msg = ~r"expected CalcMock.add/2 to be called 2 times" 315 | 316 | assert_raise Mox.UnexpectedCallError, msg, fn -> 317 | CalcMock.add(2, 3) == 5 318 | end 319 | end 320 | 321 | test "raises if all expectations are consumed, even when a stub is defined" do 322 | stub(CalcMock, :add, fn _, _ -> :stub end) 323 | 324 | expect(CalcMock, :add, 1, fn _, _ -> :expected end) 325 | assert CalcMock.add(2, 3) == :expected 326 | 327 | assert_raise Mox.UnexpectedCallError, fn -> 328 | CalcMock.add(2, 3) 329 | end 330 | end 331 | 332 | test "raises if you try to add expectations from non global process" do 333 | set_mox_global() 334 | 335 | Task.async(fn -> 336 | msg = 337 | ~r"Only the process that set Mox to global can set expectations/stubs in global mode" 338 | 339 | assert_raise ArgumentError, msg, fn -> 340 | CalcMock 341 | |> expect(:add, fn _, _ -> :expected end) 342 | end 343 | end) 344 | |> Task.await() 345 | end 346 | end 347 | 348 | describe "deny/3" do 349 | test "allows asserting that function is not called" do 350 | deny(CalcMock, :add, 2) 351 | 352 | msg = ~r"expected CalcMock.add/2 to be called 0 times but it has been called once" 353 | 354 | assert_raise Mox.UnexpectedCallError, msg, fn -> 355 | CalcMock.add(2, 3) == 5 356 | end 357 | end 358 | 359 | test "raises if a non-mock is given" do 360 | assert_raise ArgumentError, ~r"could not load module Unknown", fn -> 361 | deny(Unknown, :add, 2) 362 | end 363 | 364 | assert_raise ArgumentError, ~r"module String is not a mock", fn -> 365 | deny(String, :add, 2) 366 | end 367 | end 368 | 369 | test "raises if function is not in behaviour" do 370 | assert_raise ArgumentError, ~r"unknown function oops/2 for mock CalcMock", fn -> 371 | deny(CalcMock, :oops, 2) 372 | end 373 | 374 | assert_raise ArgumentError, ~r"unknown function add/3 for mock CalcMock", fn -> 375 | deny(CalcMock, :add, 3) 376 | end 377 | end 378 | 379 | test "raises even when a stub is defined" do 380 | stub(CalcMock, :add, fn _, _ -> :stub end) 381 | deny(CalcMock, :add, 2) 382 | 383 | assert_raise Mox.UnexpectedCallError, fn -> 384 | CalcMock.add(2, 3) 385 | end 386 | end 387 | 388 | test "raises if you try to add expectations from non global process" do 389 | set_mox_global() 390 | 391 | Task.async(fn -> 392 | msg = 393 | ~r"Only the process that set Mox to global can set expectations/stubs in global mode" 394 | 395 | assert_raise ArgumentError, msg, fn -> 396 | deny(CalcMock, :add, 2) 397 | end 398 | end) 399 | |> Task.await() 400 | end 401 | end 402 | 403 | describe "verify!/0" do 404 | test "verifies all mocks for the current process in private mode" do 405 | set_mox_private() 406 | 407 | verify!() 408 | expect(CalcMock, :add, fn x, y -> x + y end) 409 | expect(SciCalcOnlyMock, :exponent, fn x, y -> x * y end) 410 | 411 | error = assert_raise(Mox.VerificationError, &verify!/0) 412 | 413 | assert error.message =~ 414 | ~r"expected CalcMock.add/2 to be invoked once but it was invoked 0 times" 415 | 416 | assert error.message =~ 417 | ~r"expected SciCalcOnlyMock.exponent/2 to be invoked once but it was invoked 0 times" 418 | 419 | CalcMock.add(2, 3) 420 | error = assert_raise(Mox.VerificationError, &verify!/0) 421 | refute error.message =~ ~r"expected CalcMock.add/2" 422 | 423 | assert error.message =~ 424 | ~r"expected SciCalcOnlyMock.exponent/2 to be invoked once but it was invoked 0 times" 425 | 426 | SciCalcOnlyMock.exponent(2, 3) 427 | verify!() 428 | 429 | # Adding another expected call makes verification fail again 430 | expect(CalcMock, :add, fn x, y -> x + y end) 431 | error = assert_raise(Mox.VerificationError, &verify!/0) 432 | 433 | assert error.message =~ 434 | ~r"expected CalcMock.add/2 to be invoked 2 times but it was invoked once" 435 | 436 | refute error.message =~ ~r"expected SciCalcOnlyMock.exponent/2" 437 | end 438 | 439 | test "verifies all mocks for the current process in global mode" do 440 | set_mox_global() 441 | 442 | verify!() 443 | expect(CalcMock, :add, fn x, y -> x + y end) 444 | expect(SciCalcOnlyMock, :exponent, fn x, y -> x * y end) 445 | 446 | error = assert_raise(Mox.VerificationError, &verify!/0) 447 | 448 | assert error.message =~ 449 | ~r"expected CalcMock.add/2 to be invoked once but it was invoked 0 times" 450 | 451 | assert error.message =~ 452 | ~r"expected SciCalcOnlyMock.exponent/2 to be invoked once but it was invoked 0 times" 453 | 454 | Task.async(fn -> SciCalcOnlyMock.exponent(2, 4) end) 455 | |> Task.await() 456 | 457 | error = assert_raise(Mox.VerificationError, &verify!/0) 458 | 459 | assert error.message =~ 460 | ~r"expected CalcMock.add/2 to be invoked once but it was invoked 0 times" 461 | 462 | refute error.message =~ ~r"expected SciCalcOnlyMock.exponent/2" 463 | 464 | Task.async(fn -> CalcMock.add(5, 6) end) 465 | |> Task.await() 466 | 467 | verify!() 468 | 469 | expect(CalcMock, :add, fn x, y -> x + y end) 470 | error = assert_raise(Mox.VerificationError, &verify!/0) 471 | 472 | assert error.message =~ 473 | ~r"expected CalcMock.add/2 to be invoked 2 times but it was invoked once" 474 | 475 | refute error.message =~ ~r"expected SciCalcOnlyMock.exponent/2" 476 | end 477 | end 478 | 479 | describe "verify!/1" do 480 | test "verifies all mocks for the current process in private mode" do 481 | set_mox_private() 482 | 483 | verify!(CalcMock) 484 | verify!(SciCalcOnlyMock) 485 | expect(CalcMock, :add, fn x, y -> x + y end) 486 | expect(SciCalcOnlyMock, :exponent, fn x, y -> x * y end) 487 | 488 | error = assert_raise(Mox.VerificationError, fn -> verify!(CalcMock) end) 489 | 490 | assert error.message =~ 491 | ~r"expected CalcMock.add/2 to be invoked once but it was invoked 0 times" 492 | 493 | refute error.message =~ ~r"expected SciCalcOnlyMock.exponent/2" 494 | 495 | error = assert_raise(Mox.VerificationError, fn -> verify!(SciCalcOnlyMock) end) 496 | 497 | assert error.message =~ 498 | ~r"expected SciCalcOnlyMock.exponent/2 to be invoked once but it was invoked 0 times" 499 | 500 | refute error.message =~ ~r"expected CalcMock.add/2" 501 | 502 | CalcMock.add(2, 3) 503 | verify!(CalcMock) 504 | 505 | error = assert_raise(Mox.VerificationError, fn -> verify!(SciCalcOnlyMock) end) 506 | 507 | assert error.message =~ 508 | ~r"expected SciCalcOnlyMock.exponent/2 to be invoked once but it was invoked 0 times" 509 | 510 | refute error.message =~ ~r"expected CalcMock.add/2" 511 | 512 | SciCalcOnlyMock.exponent(2, 3) 513 | verify!(CalcMock) 514 | verify!(SciCalcOnlyMock) 515 | 516 | expect(CalcMock, :add, fn x, y -> x + y end) 517 | error = assert_raise Mox.VerificationError, fn -> verify!(CalcMock) end 518 | 519 | assert error.message =~ 520 | ~r"expected CalcMock.add/2 to be invoked 2 times but it was invoked once" 521 | 522 | refute error.message =~ ~r"expected SciCalcOnlyMock.exponent/2" 523 | verify!(SciCalcOnlyMock) 524 | end 525 | 526 | test "verifies all mocks for current process in global mode" do 527 | set_mox_global() 528 | 529 | verify!(CalcMock) 530 | verify!(SciCalcOnlyMock) 531 | expect(CalcMock, :add, fn x, y -> x + y end) 532 | expect(SciCalcOnlyMock, :exponent, fn x, y -> x * y end) 533 | 534 | error = assert_raise(Mox.VerificationError, fn -> verify!(CalcMock) end) 535 | 536 | assert error.message =~ 537 | ~r"expected CalcMock.add/2 to be invoked once but it was invoked 0 times" 538 | 539 | refute error.message =~ ~r"expected SciCalcOnlyMock.exponent/2" 540 | 541 | error = assert_raise(Mox.VerificationError, fn -> verify!(SciCalcOnlyMock) end) 542 | 543 | assert error.message =~ 544 | ~r"expected SciCalcOnlyMock.exponent/2 to be invoked once but it was invoked 0 times" 545 | 546 | refute error.message =~ ~r"expected CalcMock.add/2" 547 | 548 | Task.async(fn -> CalcMock.add(2, 3) end) 549 | |> Task.await() 550 | 551 | verify!(CalcMock) 552 | 553 | error = assert_raise(Mox.VerificationError, fn -> verify!(SciCalcOnlyMock) end) 554 | 555 | assert error.message =~ 556 | ~r"expected SciCalcOnlyMock.exponent/2 to be invoked once but it was invoked 0 times" 557 | 558 | refute error.message =~ ~r"expected CalcMock.add/2" 559 | 560 | SciCalcOnlyMock.exponent(2, 3) 561 | verify!(CalcMock) 562 | verify!(SciCalcOnlyMock) 563 | 564 | expect(CalcMock, :add, fn x, y -> x + y end) 565 | 566 | error = assert_raise(Mox.VerificationError, &verify!/0) 567 | 568 | assert error.message =~ 569 | ~r"expected CalcMock.add/2 to be invoked 2 times but it was invoked once" 570 | 571 | refute error.message =~ ~r"expected SciCalcOnlyMock.exponent/2" 572 | 573 | verify!(SciCalcOnlyMock) 574 | end 575 | 576 | test "raises if a non-mock is given" do 577 | assert_raise ArgumentError, ~r"could not load module Unknown", fn -> 578 | verify!(Unknown) 579 | end 580 | 581 | assert_raise ArgumentError, ~r"module String is not a mock", fn -> 582 | verify!(String) 583 | end 584 | end 585 | end 586 | 587 | describe "verify_on_exit!/0" do 588 | setup :verify_on_exit! 589 | 590 | test "verifies all mocks even if none is used in private mode" do 591 | set_mox_private() 592 | :ok 593 | end 594 | 595 | test "verifies all mocks for the current process on exit in private mode" do 596 | set_mox_private() 597 | 598 | expect(CalcMock, :add, fn x, y -> x + y end) 599 | assert CalcMock.add(2, 3) == 5 600 | end 601 | 602 | test "verifies all mocks for the current process on exit with previous verification in private mode" do 603 | set_mox_private() 604 | 605 | verify!() 606 | expect(CalcMock, :add, fn x, y -> x + y end) 607 | assert CalcMock.add(2, 3) == 5 608 | end 609 | 610 | test "verifies all mocks even if none is used in global mode" do 611 | set_mox_global() 612 | :ok 613 | end 614 | 615 | test "verifies all mocks for current process on exit in global mode" do 616 | set_mox_global() 617 | 618 | expect(CalcMock, :add, fn x, y -> x + y end) 619 | 620 | task = 621 | Task.async(fn -> 622 | assert CalcMock.add(2, 3) == 5 623 | end) 624 | 625 | Task.await(task) 626 | end 627 | 628 | test "verifies all mocks for the current process on exit with previous verification in global mode" do 629 | set_mox_global() 630 | 631 | verify!() 632 | expect(CalcMock, :add, fn x, y -> x + y end) 633 | 634 | task = 635 | Task.async(fn -> 636 | assert CalcMock.add(2, 3) == 5 637 | end) 638 | 639 | Task.await(task) 640 | end 641 | 642 | test "raises if the mocks are not called" do 643 | pid = self() 644 | 645 | verify_on_exit!() 646 | 647 | # This replicates exactly what verify_on_exit/1 does, but it adds an assertion 648 | # in there. There's no easy way to test that something gets raised in an on_exit 649 | # callback. 650 | ExUnit.Callbacks.on_exit(Mox, fn -> 651 | assert_raise Mox.VerificationError, fn -> 652 | Mox.__verify_mock_or_all__(pid, :all) 653 | NimbleOwnership.cleanup_owner({:global, Mox.Server}, pid) 654 | end 655 | end) 656 | 657 | set_mox_private() 658 | 659 | expect(CalcMock, :add, fn x, y -> x + y end) 660 | end 661 | end 662 | 663 | describe "stub/3" do 664 | test "allows repeated invocations" do 665 | in_all_modes(fn -> 666 | stub(CalcMock, :add, fn x, y -> x + y end) 667 | assert CalcMock.add(1, 2) == 3 668 | assert CalcMock.add(3, 4) == 7 669 | end) 670 | end 671 | 672 | test "does not fail verification if not called" do 673 | in_all_modes(fn -> 674 | stub(CalcMock, :add, fn x, y -> x + y end) 675 | verify!() 676 | end) 677 | end 678 | 679 | test "gives expected calls precedence" do 680 | in_all_modes(fn -> 681 | CalcMock 682 | |> stub(:add, fn x, y -> x + y end) 683 | |> expect(:add, fn _, _ -> :expected end) 684 | 685 | assert CalcMock.add(1, 1) == :expected 686 | verify!() 687 | end) 688 | end 689 | 690 | test "a stub declared after an expect is invoked after all expectations are fulfilled" do 691 | in_all_modes(fn -> 692 | CalcMock 693 | |> expect(:add, 2, fn _, _ -> :expected end) 694 | |> stub(:add, fn _x, _y -> :stub end) 695 | 696 | assert CalcMock.add(1, 1) == :expected 697 | assert CalcMock.add(1, 1) == :expected 698 | assert CalcMock.add(1, 1) == :stub 699 | verify!() 700 | end) 701 | end 702 | 703 | test "overwrites earlier stubs" do 704 | in_all_modes(fn -> 705 | CalcMock 706 | |> stub(:add, fn x, y -> x + y end) 707 | |> stub(:add, fn _x, _y -> 42 end) 708 | 709 | assert CalcMock.add(1, 1) == 42 710 | end) 711 | end 712 | 713 | test "works with multiple behaviours" do 714 | in_all_modes(fn -> 715 | SciCalcMock 716 | |> stub(:add, fn x, y -> x + y end) 717 | |> stub(:exponent, fn x, y -> :math.pow(x, y) end) 718 | 719 | assert SciCalcMock.add(1, 1) == 2 720 | assert SciCalcMock.exponent(2, 3) == 8 721 | end) 722 | end 723 | 724 | test "raises if a non-mock is given" do 725 | in_all_modes(fn -> 726 | assert_raise ArgumentError, ~r"could not load module Unknown", fn -> 727 | stub(Unknown, :add, fn x, y -> x + y end) 728 | end 729 | 730 | assert_raise ArgumentError, ~r"module String is not a mock", fn -> 731 | stub(String, :add, fn x, y -> x + y end) 732 | end 733 | end) 734 | end 735 | 736 | test "raises if function is not in behaviour" do 737 | in_all_modes(fn -> 738 | assert_raise ArgumentError, ~r"unknown function oops/2 for mock CalcMock", fn -> 739 | stub(CalcMock, :oops, fn x, y -> x + y end) 740 | end 741 | 742 | assert_raise ArgumentError, ~r"unknown function add/3 for mock CalcMock", fn -> 743 | stub(CalcMock, :add, fn x, y, z -> x + y + z end) 744 | end 745 | end) 746 | end 747 | end 748 | 749 | describe "stub_with/2" do 750 | defmodule CalcImplementation do 751 | @behaviour Calculator 752 | def add(x, y), do: x + y 753 | def mult(x, y), do: x * y 754 | end 755 | 756 | defmodule SciCalcImplementation do 757 | @behaviour Calculator 758 | def add(x, y), do: x + y 759 | def mult(x, y), do: x * y 760 | 761 | @behaviour ScientificCalculator 762 | def exponent(x, y), do: :math.pow(x, y) 763 | end 764 | 765 | defmodule SciCalcFullImplementation do 766 | @behaviour Calculator 767 | def add(x, y), do: x + y 768 | def mult(x, y), do: x * y 769 | 770 | @behaviour ScientificCalculator 771 | def exponent(x, y), do: :math.pow(x, y) 772 | def sin(x), do: :math.sin(x) 773 | end 774 | 775 | test "can override stubs" do 776 | in_all_modes(fn -> 777 | stub_with(CalcMock, CalcImplementation) 778 | |> expect(:add, fn 1, 2 -> 4 end) 779 | 780 | assert CalcMock.add(1, 2) == 4 781 | verify!() 782 | end) 783 | end 784 | 785 | test "stubs all functions with functions from a module" do 786 | in_all_modes(fn -> 787 | stub_with(CalcMock, CalcImplementation) 788 | assert CalcMock.add(1, 2) == 3 789 | assert CalcMock.add(3, 4) == 7 790 | assert CalcMock.mult(2, 2) == 4 791 | assert CalcMock.mult(3, 4) == 12 792 | end) 793 | end 794 | 795 | test "stubs functions which are optional callbacks" do 796 | in_all_modes(fn -> 797 | stub_with(SciCalcMock, SciCalcFullImplementation) 798 | assert SciCalcMock.add(1, 2) == 3 799 | assert SciCalcMock.mult(3, 4) == 12 800 | assert SciCalcMock.exponent(2, 10) == 1024 801 | assert SciCalcMock.sin(0) == 0.0 802 | end) 803 | end 804 | 805 | test "skips undefined functions which are optional callbacks" do 806 | in_all_modes(fn -> 807 | stub_with(SciCalcMockWithoutOptional, SciCalcImplementation) 808 | assert SciCalcMockWithoutOptional.add(1, 2) == 3 809 | assert SciCalcMockWithoutOptional.mult(3, 4) == 12 810 | assert SciCalcMockWithoutOptional.exponent(2, 10) == 1024 811 | 812 | assert_raise UndefinedFunctionError, fn -> 813 | SciCalcMockWithoutOptional.sin(1) 814 | end 815 | end) 816 | end 817 | 818 | test "Leaves behaviours not implemented by the module un-stubbed" do 819 | in_all_modes(fn -> 820 | stub_with(SciCalcMock, CalcImplementation) 821 | assert SciCalcMock.add(1, 2) == 3 822 | assert SciCalcMock.mult(3, 4) == 12 823 | 824 | assert_raise Mox.UnexpectedCallError, fn -> 825 | SciCalcMock.exponent(2, 10) 826 | end 827 | end) 828 | end 829 | 830 | test "can stub multiple behaviours from a single module" do 831 | in_all_modes(fn -> 832 | stub_with(SciCalcMock, SciCalcImplementation) 833 | assert SciCalcMock.add(1, 2) == 3 834 | assert SciCalcMock.mult(3, 4) == 12 835 | assert SciCalcMock.exponent(2, 10) == 1024 836 | end) 837 | end 838 | end 839 | 840 | describe "allow/3" do 841 | setup :set_mox_private 842 | setup :verify_on_exit! 843 | 844 | test "allows different processes to share mocks from parent process" do 845 | parent_pid = self() 846 | 847 | {:ok, child_pid} = 848 | start_link_no_callers(fn -> 849 | receive do 850 | :call_mock -> 851 | add_result = CalcMock.add(1, 1) 852 | mult_result = CalcMock.mult(1, 1) 853 | send(parent_pid, {:verify, add_result, mult_result}) 854 | end 855 | end) 856 | 857 | CalcMock 858 | |> expect(:add, fn _, _ -> :expected end) 859 | |> stub(:mult, fn _, _ -> :stubbed end) 860 | |> allow(self(), child_pid) 861 | 862 | send(child_pid, :call_mock) 863 | 864 | assert_receive {:verify, add_result, mult_result} 865 | assert add_result == :expected 866 | assert mult_result == :stubbed 867 | end 868 | 869 | test "allows different processes to share mocks from child process" do 870 | parent_pid = self() 871 | 872 | CalcMock 873 | |> expect(:add, fn _, _ -> :expected end) 874 | |> stub(:mult, fn _, _ -> :stubbed end) 875 | 876 | async_no_callers(fn -> 877 | CalcMock 878 | |> allow(parent_pid, self()) 879 | 880 | assert CalcMock.add(1, 1) == :expected 881 | assert CalcMock.mult(1, 1) == :stubbed 882 | end) 883 | |> Task.await() 884 | end 885 | 886 | test "allowances are transitive" do 887 | parent_pid = self() 888 | 889 | {:ok, child_pid} = 890 | start_link_no_callers(fn -> 891 | receive do 892 | :call_mock -> 893 | add_result = CalcMock.add(1, 1) 894 | mult_result = CalcMock.mult(1, 1) 895 | send(parent_pid, {:verify, add_result, mult_result}) 896 | end 897 | end) 898 | 899 | {:ok, transitive_pid} = 900 | Task.start_link(fn -> 901 | receive do 902 | :allow_mock -> 903 | CalcMock 904 | |> allow(self(), child_pid) 905 | 906 | send(child_pid, :call_mock) 907 | end 908 | end) 909 | 910 | CalcMock 911 | |> expect(:add, fn _, _ -> :expected end) 912 | |> stub(:mult, fn _, _ -> :stubbed end) 913 | |> allow(self(), transitive_pid) 914 | 915 | send(transitive_pid, :allow_mock) 916 | 917 | receive do 918 | {:verify, add_result, mult_result} -> 919 | assert add_result == :expected 920 | assert mult_result == :stubbed 921 | verify!() 922 | after 923 | 1000 -> verify!() 924 | end 925 | end 926 | 927 | test "allowances are reclaimed if the owner process dies" do 928 | parent_pid = self() 929 | 930 | {_pid, ref} = 931 | spawn_monitor(fn -> 932 | CalcMock 933 | |> expect(:add, fn _, _ -> :expected end) 934 | |> stub(:mult, fn _, _ -> :stubbed end) 935 | |> allow(self(), parent_pid) 936 | end) 937 | 938 | receive do 939 | {:DOWN, ^ref, _, _, _} -> :ok 940 | end 941 | 942 | assert_raise Mox.UnexpectedCallError, fn -> 943 | CalcMock.add(1, 1) 944 | end 945 | 946 | CalcMock 947 | |> expect(:add, 1, fn x, y -> x + y end) 948 | 949 | assert CalcMock.add(1, 1) == 2 950 | end 951 | 952 | test "allowances support locally registered processes" do 953 | parent_pid = self() 954 | process_name = :test_process 955 | 956 | {:ok, child_pid} = 957 | Task.start_link(fn -> 958 | receive do 959 | :call_mock -> 960 | add_result = CalcMock.add(1, 1) 961 | send(parent_pid, {:verify, add_result}) 962 | end 963 | end) 964 | 965 | Process.register(child_pid, process_name) 966 | 967 | CalcMock 968 | |> expect(:add, fn _, _ -> :expected end) 969 | |> allow(self(), process_name) 970 | 971 | send(:test_process, :call_mock) 972 | 973 | assert_receive {:verify, add_result} 974 | assert add_result == :expected 975 | end 976 | 977 | test "allowances support processes registered through a Registry" do 978 | defmodule CalculatorServer do 979 | use GenServer 980 | 981 | def init(args) do 982 | {:ok, args} 983 | end 984 | 985 | def handle_call(:call_mock, _from, []) do 986 | add_result = CalcMock.add(1, 1) 987 | {:reply, add_result, []} 988 | end 989 | end 990 | 991 | {:ok, _} = Registry.start_link(keys: :unique, name: Registry.Test) 992 | name = {:via, Registry, {Registry.Test, :test_process}} 993 | {:ok, _} = GenServer.start_link(CalculatorServer, [], name: name) 994 | 995 | CalcMock 996 | |> expect(:add, fn _, _ -> :expected end) 997 | |> allow(self(), name) 998 | 999 | add_result = GenServer.call(name, :call_mock) 1000 | assert add_result == :expected 1001 | end 1002 | 1003 | test "allowances support lazy calls for processes registered through a Registry" do 1004 | defmodule CalculatorServer_Lazy do 1005 | use GenServer 1006 | 1007 | def init(args) do 1008 | {:ok, args} 1009 | end 1010 | 1011 | def handle_call(:call_mock, _from, []) do 1012 | add_result = CalcMock.add(1, 1) 1013 | {:reply, add_result, []} 1014 | end 1015 | end 1016 | 1017 | {:ok, _} = Registry.start_link(keys: :unique, name: Registry.Test) 1018 | name = {:via, Registry, {Registry.Test, :test_process_lazy}} 1019 | 1020 | CalcMock 1021 | |> expect(:add, fn _, _ -> :expected end) 1022 | |> allow(self(), fn -> GenServer.whereis(name) end) 1023 | 1024 | {:ok, _} = GenServer.start_link(CalculatorServer_Lazy, [], name: name) 1025 | add_result = GenServer.call(name, :call_mock) 1026 | assert add_result == :expected 1027 | end 1028 | 1029 | test "raises if you try to allow itself" do 1030 | assert_raise ArgumentError, "owner_pid and allowed_pid must be different", fn -> 1031 | CalcMock 1032 | |> allow(self(), self()) 1033 | end 1034 | end 1035 | 1036 | test "raises if you try to allow already allowed process" do 1037 | {:ok, child_pid} = Task.start_link(fn -> Process.sleep(:infinity) end) 1038 | 1039 | CalcMock 1040 | |> allow(self(), child_pid) 1041 | |> allow(self(), child_pid) 1042 | 1043 | Task.async(fn -> 1044 | assert_raise ArgumentError, ~r"it is already allowed by", fn -> 1045 | CalcMock 1046 | |> allow(self(), child_pid) 1047 | end 1048 | end) 1049 | |> Task.await() 1050 | end 1051 | 1052 | test "raises if you try to allow process with existing expectations set" do 1053 | parent_pid = self() 1054 | 1055 | {:ok, pid} = 1056 | Task.start_link(fn -> 1057 | CalcMock 1058 | |> expect(:add, fn _, _ -> :expected end) 1059 | 1060 | send(parent_pid, :ready) 1061 | Process.sleep(:infinity) 1062 | end) 1063 | 1064 | assert_receive :ready 1065 | 1066 | assert_raise ArgumentError, ~r"the process has already defined its own expectations", fn -> 1067 | CalcMock 1068 | |> allow(self(), pid) 1069 | end 1070 | end 1071 | 1072 | test "raises if you try to define expectations on allowed process" do 1073 | parent_pid = self() 1074 | 1075 | Task.start_link(fn -> 1076 | CalcMock 1077 | |> allow(self(), parent_pid) 1078 | 1079 | send(parent_pid, :ready) 1080 | Process.sleep(:infinity) 1081 | end) 1082 | 1083 | assert_receive :ready 1084 | 1085 | assert_raise ArgumentError, ~r"because the process has been allowed by", fn -> 1086 | CalcMock 1087 | |> expect(:add, fn _, _ -> :expected end) 1088 | end 1089 | end 1090 | 1091 | test "is ignored if you allow process while in global mode" do 1092 | set_mox_global() 1093 | {:ok, child_pid} = Task.start_link(fn -> Process.sleep(:infinity) end) 1094 | 1095 | Task.async(fn -> 1096 | mock = CalcMock 1097 | assert allow(mock, self(), child_pid) == mock 1098 | end) 1099 | |> Task.await() 1100 | end 1101 | end 1102 | 1103 | describe "set_mox_global/1" do 1104 | test "raises if the test case is async" do 1105 | message = ~r/Mox cannot be set to global mode when the ExUnit case is async/ 1106 | assert_raise RuntimeError, message, fn -> set_mox_global(%{async: true}) end 1107 | end 1108 | end 1109 | 1110 | defp async_no_callers(fun) do 1111 | Task.async(fn -> 1112 | Process.delete(:"$callers") 1113 | fun.() 1114 | end) 1115 | end 1116 | 1117 | defp start_link_no_callers(fun) do 1118 | Task.start_link(fn -> 1119 | Process.delete(:"$callers") 1120 | fun.() 1121 | end) 1122 | end 1123 | end 1124 | -------------------------------------------------------------------------------- /test/support/behaviours.ex: -------------------------------------------------------------------------------- 1 | defmodule Calculator do 2 | @callback add(integer(), integer()) :: integer() 3 | @callback mult(integer(), integer()) :: integer() 4 | end 5 | 6 | defmodule ScientificCalculator do 7 | @callback exponent(integer(), integer()) :: integer() 8 | @callback sin(integer()) :: float() 9 | @optional_callbacks [sin: 1] 10 | end 11 | -------------------------------------------------------------------------------- /test/support/mocks.ex: -------------------------------------------------------------------------------- 1 | Mox.defmock(CalcMock, for: Calculator) 2 | Mox.defmock(SciCalcOnlyMock, for: ScientificCalculator) 3 | Mox.defmock(SciCalcMock, for: [Calculator, ScientificCalculator]) 4 | 5 | Mox.defmock(SciCalcMockWithoutOptional, 6 | for: [Calculator, ScientificCalculator], 7 | skip_optional_callbacks: true 8 | ) 9 | 10 | Mox.defmock(MyMockWithoutModuledoc, for: Calculator) 11 | Mox.defmock(MyMockWithFalseModuledoc, for: Calculator, moduledoc: false) 12 | Mox.defmock(MyMockWithStringModuledoc, for: Calculator, moduledoc: "hello world") 13 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | excludes = 2 | [ 3 | {"< 1.7.0", [:requires_code_fetch_docs]}, 4 | {"< 1.8.0", [:requires_caller_tracking]} 5 | ] 6 | |> Enum.flat_map(fn {version, tags} -> 7 | if Version.match?(System.version(), version), do: tags, else: [] 8 | end) 9 | 10 | ExUnit.start(exclude: excludes) 11 | --------------------------------------------------------------------------------