Is this a better way to test Elixir?

Hmm, so perhaps Mox is somehow using the process dictionary to keep expectations isolated? I am not trying to be dense, perhaps I am just getting tripped up on the wording. I keep hearing how Mox allows for separate implementations when I am worried about separate state. Over the weekend I should have time to create a prototype using Mox, so hopefully I will either address my own concerns or be able to articulate them more precisely.

I intend to get this example under 100% test coverage

defmodule Hello do
  def main(args) do
      time_unit = :microsecond
      microseconds_before = System.monotonic_time time_unit
      target = args |> List.first |> File.read!
      IO.puts "Hello, #{target}!"
      microseconds_after = System.monotonic_time time_unit
      microseconds_duration = microseconds_after - microseconds_before
      IO.puts "Took #{microseconds_duration} microseconds"
  end
end

Also, I don’t mean to be repetitive, I was addressing each reply in order. I do appreciate you all taking the time to help me understand.

4 Likes

I did watch the linked video and read through the Mox dox, so please know that I am taking all of your feedback seriously. Hopefully it becomes more clear once I have time to work on an example this weekend.

1 Like

100%, it is doing exactly this.

Right so the bit that I think is tripping you up is that that there are two distinct questions:

  1. What expectation functions should execute for the current caller pid.
  2. What should those functions return when they model something stateful.

Thing (1) there is where Mox’s pdict magic handles executing different functions for different caller pids. But the thing that makes all this work is that those functions have no intrinsic statefulness at all, which makes it trivial for you to add in whatever statefulness you want to adjust (2) in whatever way is best for your tests and the stateful system you want to manage. So you can do things like:

# In a test
{:ok, time_pid} = MyTestTime.start_link(initial_time)

Mox.expect(TimeMock, :monotonic_time, 2, fn ->
  MyTestTime.get_current_time(time_pid)
end)

# run some that calls the MyApp.Time.get_current_time function, where MyApp.Time is a behavior.
assert blah

# Let's move time now!

MyTestTime.set_time(time_pid, my_new_time)

# run more code

I’ll try to get a more complete example for your code in a bit.

Fair enough! And I am sorry if I came on strong earlier. Your detailed replies are genuinely helpful, I feel like we are making progress here.

3 Likes

OK here we go!

This has your main function. As you can see I have swapped out the System IO and File to the general IO behavior module I define here. https://github.com/benwilson512/mox_demo/blob/main/lib/io.ex

As I note in that module, it’s a bit simplistic to put all the IO functions in one module. In the real world you’d probably define multiple behaviors, but I wanted to keep this simple.

Then we have the test:

As you can see I can define test specific expectations. In the end I didn’t even really need to manage any state, since the ability to chain expectations meant that when you call eg monotonic_time twice I just define two expectations, one with the first value, and one with the second.

I don’t have additional tests in there right now but these expectations are 100% isolated from any other tests that would be happening.

One nice consequence of this approach is that backend being parameterized you still get strong compiler and language server support since it resolves to a compile time value.

This is barely scratching the surface of what’s possible though and ironically, a lot of what’s possible is made possible by how minimalist Mox is. Here is the same test set up a different way where it’s more interactive. Basically instead of setting up all the IO ahead of time and calling main(), we put main in a task and then have it basically “talk to” our test process as if it is the IO world:

All I’m doing here is combining core Elixir primitives like send and receive with Mox. Mox doesn’t really have any state to track here other than knowing which functions to call for this particular invocation of main. Then the “state” of the IO is in this case managed by my test process itself by sending messages to the mock at the appropriate times.

And of course you’ll note that you can just run mix test in the project root directory and these two test files will run at the same time and do not interfere. Both approaches are perfectly valid. I tend to favor the Mox.expect approach when I just have some outside system that I need a value or two from, or that I want to assert we pushed a value to. I tend to use the second approach if I’m writing more of a “simulator” test where there is some IO heavy piece of code and I want to step through it. This particular example makes that look sort of verbose but that setup block is generic. Once you write your little stub you’re done, and each test just gets to really focus on the interaction between the function under testing and the IO.

14 Likes

The compile_env! tip is great –

Does this mean if you have:

@backend.i_dont_exist()

You’ll get a compiler error?

Yup! You get the standard warning you get when a function doesn’t exist

1 Like

wow. That’s awesome.

I’m going to put out a blog post about this - can I tag you?

2 Likes

Certainly! Feel free to use any of the code in that repo as well!

3 Likes

If you don’t have sources of state as a behavior, it will not be testable with Mox

This is not entirely true. The mox documentation is confusing, it says “no ad-hoc mock without a behaviour”, which is true, but you can certainly create an ad-hoc behaviour and build your mock on top of that.

So even if your source of state doesn’t ship with a behaviour API no one will stop you from building you own that uh just so happens to look like the Module you’re building a mock for. Module calls are just apply/3 and they don’t enforce behaviour besides throwing if the function call doesn’t exist.

1 Like

I mean, that depends entirely on what you choose to implement with Mox. If you don’t have sources of state as a behavior, it will not be testable with Mox.

I do agree though that once you’ve got Mox in the mix there are many options available.

Thanks so much for putting the work in, that was really helpful for my prototyping. You can see my various attempts here:

Now that I got the mechanics down I can try to figure out which I like best and why, but I am too tired for analysis now. I wanted to post my results here in case anyone else is interested.

5 Likes

Glad you found it helpful! The one suggestion I would make to your mox code is when you have the implementation modules eg https://github.com/SeanShubin/testability_using_mox/blob/master/lib/file_native.ex be sure to do @behaviour FileProxy because then you get compiler checks that the FileNative module has in fact implemented all of the required functions in that behavior.

Otherwise, looking forward to your thoughts!

Thanks, I have made the update.

I have posted my analysis here:
https://elixirforum.com/t/analysis-of-testing-styles-in-elixir/57329

I found the intention of my analysis sufficiently different than my intention for the discussion here to warrant a separate thread.