doodloo

doodloo

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?

Showing Posts 1 to 4

doodloo

doodloo OP

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
escobera

escobera

@doodloo I loved the approach, but I’m having trouble on my project to set it up.

I’ve defined my base TestCase as such:

defmodule ProcessosApi.TestCase do
  use ExUnit.CaseTemplate

  import Mox, only: [stub_with: 2]

  setup _tags do
    ProcessosApiKafka.PublicadorMock
    |> stub_with(ProcessosApiKafka.Publicador)

    :ok
  end
end

But when I try to use it, instead of ExUnit.CaseTemplate on data_case I get

== Compilation error in file test/support/data_case.ex ==
** (RuntimeError) cannot use ExUnit.Case without starting the ExUnit application, please call ExUnit.start() or explicitly start the :ex_unit app

Can you share a little more of how are you configuring your tests?

brettbeatty

brettbeatty

That warning would suggest your app isn’t starting up :ex_unit.

If you get that error when running tests, do you have an ExUnit.start() # possibly with args in your test/test_helper.exs?

escobera

escobera

This is the contents of my test/test_helper.exs

ExUnit.start()

Ecto.Adapters.SQL.Sandbox.mode(ProcessosApi.Repo, :manual)

Mox.defmock(ProcessosApiKafka.PublicadorMock, for: ProcessosApiKafka.Publicador)

Update:

In order for it to work on my side I had to change all of test/suport/*_case.ex files to have

    import Mox, only: [stub_with: 2]

    ProcessosApiKafka.PublicadorMock
    |> stub_with(ProcessosApiKafka.Publicador)

Inside its setup block, for instance, inside test/support/data_case.ex

setup tags do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(ProcessosApi.Repo)

    unless tags[:async] do
      Ecto.Adapters.SQL.Sandbox.mode(ProcessosApi.Repo, {:shared, self()})
    end

    import Mox, only: [stub_with: 2]

    ProcessosApiKafka.PublicadorMock
    |> stub_with(ProcessosApiKafka.Publicador)

    {:ok, conn: Phoenix.ConnTest.build_conn()}
  end

This seems pretty different from what @doodloo showed, but I couldn’t make it compile the other way. Do you have any idea what I’m doing wrong here?

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
leoschweizer
Hi, we run different LLM models across a cluster, with each node loading a configurable subset. Model-specific queues let nodes consume ...
New
csmr
Hi all, I’m new here and would like to hear peoples views on my Elixir/Phoenix based game-engine! :smiley: Mutonex gamedesign was finish...
New
perzanko
Do we have maintainer of absinthe-graphql/dataloader here? We have a PR that we’d like to merge to the library. Relatively small additive...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews