doodloo

doodloo

Mocking during one test or one test module only

Consider the following scenario:

  • A Normalizer behaviour with a normalize_email function.
  • A User module, with a changeset function that should normalize the email param.
  • The User.changeset function calls a default implementation defined such as normalizer().normalize_email() - normalizer() is defined to return Application.get_env(:my_app :normalizer, Utils). It means that by default there’s a normalize_email function in the Utils module that does the job.
  • Note that many other functions in the User module use the normalizer().normalize_email() function.
  • In a mocks.ex file in the test/support folder, we Mox.defmock(NormalizerMock, for: Normalizer) and in test_helper.exs we simply Application.put_env(:my_app :normalizer, NormalizerMock).

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)?

EDIT: Basically a solution we found is to include stub(NormalizerMock, Utils) in every single other test - which is definitely not acceptable. What we’re trying to understand is how we could do the opposite - have Utils be the default implementation during all tests, except for a few in which we want to mock and expect that it’s being called. Does this make sense? Would it for example be OK to have a macro run before each test suite and bake a setup block for each test calling stub_with NormalizerMock, Utils so tests that don’t use the mock don’t have it, while tests that use the mock could still perform NormalizerMock.expect tests?

Most Liked

doodloo

doodloo

Replying to myself - something we found would work is the following.

Create a Case base module that all test modules will use instead of ExUnit.Case:

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

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

If you also have a DataCase or other helpers, don’t forget to have them use this new TestCase module too!

Then in all our test modules, we simply use MyApp.Case, so it sets default implementations instead of mocks. For example:

defmodule MyApp.Accounts.UserTest do
  # Note that his internally `use`s the `MyApp.Case` described earlier, but if our test isn't
  # a `DataCase` we would just `use MyApp.Case` instead.
  use MyApp.DataCase, async: true
  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}"

      # Note that since we called `setup :verify_on_exit!`, every `expect` will be verified, including
      # the this one:
      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

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement