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.
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.
Right so the bit that I think is tripping you up is that that there are two distinct questions:
What expectation functions should execute for the current caller pid.
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.
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.
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.
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.
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.
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.