dashbitco / mox

Mocks and explicit contracts in Elixir
1.35k stars 77 forks source link

Mock only during certain tests. #87

Closed hickscorp closed 4 years ago

hickscorp commented 4 years ago

Consider the following scenario:

Now in our user tests, this is all nice and dandy - we can expect on the mock and make sure that the correct function is called by all the User functions such as the User.changeset one.

The intent is really just to test that the User.changeset and other functions delegate the work to the Utils module - so mocking it gives us a way to verify this. We are not interested in what the Utils.normalize_email function does from these tests and the only place where the Utils.normalize_email function is tested is in its own Utils tests. In other words we just check that the NormalizerMock.normalize_email function was called exactly once with a parameter we control, and that the result of the User.changeset function includes this controlled parameter in its return.

However in other tests that target other files and modules, we would like this mock to be absent completely, and have the User.changeset function normally call onto the Utils.normalize_email function.

For example, in tests related to a registration controller (or say more integration tests), we call functions on a Registration module, itself calling the User.changeset function - and in this case we don't care to have User.changeset call a mock - instead it should call its default implementation. Of course, doing things pixel-perfect would require us to actually mock the User.changeset function in the Registration tests, but there is too much overhead here.

Is there a way to have mocks applied only to one test? To one test file? Where would all the moving pieces go (Eg where would the defmock call be, the Application.put_env call etc)?

josevalim commented 4 years ago

Hi @hickscorp! I recommend moving this discussion to the Elixir Forum, as other people can help with their input and feedback and how they tackled this.

hickscorp commented 4 years ago

Good afternoon @josevalim and thank you for the answer. Doing now.

hickscorp commented 4 years ago

For historical / documentation purposes, we found a satisfactory way of doing what we were trying to do. See https://elixirforum.com/t/mocking-during-one-test-or-one-test-module-only/29804 for more information.

Basically, we declared a common MyApp.Case module that uses ExUnit.CaseTemplate. All our test modules use it now. The MyApp.Case is in charge of stubbing all mocks with their default implementation in a setup block that runs before each test globally, something like this:

defmodule MyApp.Case do
  use ExUnit.CaseTemplate
  import Hammox, only: [stub_with: 2]

  setup _tags do
    NormalizerMock |> stub_with(Utils)
    ...
    :ok
  end
end

This way, a test that needs to check that a mock is really called once can look like this:

defmodule MyApp.Accounts.UserTest do
  use MyApp.Case, async: true
  import MyApp.Factory
  import Hammox, only: [expect: 3, verify_on_exit!: 1]

  doctest User, import: true

  setup :verify_on_exit!

  describe "changeset/2" do
    test "normalizes the email" do
      new_email = Faker.Internet.email()
      exp = "normalized_#{new_email}"

      NormalizerMock
      |> expect(:normalize_email, fn ^new_email -> exp end)

      %{changes: changes} =
        :user
        |> build
        |> User.changeset(%{email: new_email})

      assert changes == %{email: exp}
    end
  end
end

I hope this will help.