Compilation order during tests

compiling
troubleshooting

#1

I’m trying to mock ExTwitter during tests, I’m using https://github.com/plataformatec/mox for that.

In my code I use a config variable to do the Dependency Injection, like this:

config :my_app,
  # DI
  twitter_client: MyApp.TwitterClient

I’ve defined my mock in test/support/mocks.ex as per Mox docs like this

Mox.defmock(MyApp.Mocks.TwitterClient, for: MyApp.TwitterClient)

and in my test.exs config I have

config :my_app,
  # DI
  twitter_client: MyApp.Mocks.TwitterClient

Everything worked while incrementally compiling during development, however on a fresh checkout without any _build folder i get this error:

== Compilation error in file test/support/mocks.ex ==
** (ArgumentError) module MyApp.TwitterClient is not available, please pass an existing module to :for
    lib/mox.ex:92: Mox.validate_behaviour!/1
    lib/mox.ex:84: Mox.defmock/2
    (elixir) lib/kernel/parallel_compiler.ex:121: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/1

if I remove that mock line, compile and add the line back everything works. It seems like it’s trying to compile the mock file first and then the lib folder.

My elixirc_paths is ["lib", "test/support", "test/factories"] so lib comes first, any idea?

I’ve pushed a sample app with the problem in https://github.com/alex88/myapp just run mix test


#2

The Mox documentation suggests putting the Mox.defmock calls in test/test_helper.exs, that’s because the test helper is evaluated after all files are compiled. When you put the calls in the body of a compiled file it will try to check the mocked module is available at compile time which is a race condition.


#3

This is actually incorrect, and also something you probably don’t want to do as you will lose compile-time compatability check of behaviors.

The problem is slightly different - i.e. the behavior is not properly specified here. I will publish a pull request for your repo @alex88 in a few mins to show you.


#4

That’s because there is no order in the compilation process?

Anyway, having the mocked module in the test config, when I move the mock into test_helper generates:

warning: function MyApp.Mocks.TwitterClient.configure/2 is undefined (module MyApp.Mocks.TwitterClient is not available)
  lib/my_app/stream/publisher.ex:9

warning: function MyApp.Mocks.TwitterClient.update_with_media/2 is undefined (module MyApp.Mocks.TwitterClient is not available)
  lib/my_app/stream/publisher.ex:22

warning: function MyApp.Mocks.TwitterClient.update/1 is undefined (module MyApp.Mocks.TwitterClient is not available)
  lib/my_app/stream/publisher.ex:25

so as per https://hexdocs.pm/mox/Mox.html#module-compile-time-requirements I moved the mock code into support/mocks.ex

to solve that I could this in my test helper:

Mox.defmock(MyApp.Mocks.TwitterClient, for: MyApp.TwitterClient)
Application.put_env(:my_app, :twitter_client, MyApp.Mocks.TwitterClient)

but is that the correct way?


#5

hold on I’m investigating, some voodoo is going on here :smiley:


#6

Sure, just wanted to share all I tried at this point :slight_smile:


#7

Yes, modules will be compiled in the correct based on the calls or requires that is made in compile time. If you call a module at compile time the calling module will wait until the callee is compiled.

If it’s supposed to be supported to define behaviour implementations* at compile time then I think this line https://github.com/plataformatec/mox/blob/master/lib/mox.ex#L135 should call Code.ensure_compiled? instead.


#8

It’s not supposed to define new behaviours at compile time. Rather that it should create behaviour-compatible mocks based on already compiled modules, but you are right, it should be Code.ensure_compield?


#9

If you call Mox.defmock at compile time, which you do if you put it in the body of a file covered by elixirc_paths, then the function will call Code.ensure_loaded? at compile time. You will see that if you follow the stacktrace leading up to https://github.com/plataformatec/mox/blob/master/lib/mox.ex#L135. Calling Code.ensure_loaded? at compile is not safe because it’s a race to check if the module is loaded. If you instead call Code.ensure_compiled? then the compiler will wait until the given module is compiled, it works similar to require. You will notice that it works if you add a call to Code.ensure_compiled? before the call to Mox.defmock.


#10

Yes, you are right ref the cause of the issue.


#11

I can confirm that changing from Code.ensure_loaded? to Code.ensure_compiled? fixes the issue while compiling and the app tests passes


#12

I am trying to figure out why this is not happening on the app I am using mox in. But I guess it simply boils down to me being lucky and compilation order being luckily correct for me.

I submitted a pull request and this should be resolved in next version :).


#13

Mine had the problem only on clean builds, it took a while to find that was happening since I had the module to be mocked already compiled when I first added the dependency and everything was ok


#14

I figured it out why it works for me. I have 3 apps in umbrella, and I only mock the interactions where one calls another. So I suppose in my case the clean build works since the app I am about to mock is already always fully compiled (is in_umbrella dependency).


#15

Just as reference for readers, the pull request @hubertlepicki openend that includes the fix is https://github.com/plataformatec/mox/pull/17


#16

Thank you both @alex88 for reporting the issue and @ericmj for finding the solution <3


#17

I was experiencing this as well. My solution involved using use MyApp.MyBehaviour. This required that I implement def __using__(_) as so:

defmodule MyApp.MyBehaviour do
  @moduledoc """
  Behaviour definition
  """

  @type entry_attrs_t::map
  
  @callback create_entry(entry_attrs_t) :: any 

  def __using__(_) do

  end
end

and then in test/support/mocks.ex:

use MyApp.MyBehaviour
defmodule Mocks do
  @moduledoc false
end


Mox.defmock(MyApp.MyMock, for: MyApp.MyBehaviour)

kinda hacky, but not horrible…


#18

Turns out the real solution was to upgrade the version :wink:
Didn’t realize I had pinned it at 0.1.0 (C&P from web page, DOH)


#19

Turns out the real solution was to upgrade the version
Didn’t realize I had pinned it at 0.1.0 (C&P from web page, DOH)

Running mix hex.outdated every so often surfaces those things :slight_smile: