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

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
earth10
Hi, I’m just starting to build a side-project with Elixir and Phoenix and doing some basic test with Elixir alone. What strikes me is th...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
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
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
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
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39297 209
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New

We're in Beta

About us Mission Statement