SeanShubin
This is a followup to my earlier attempt here
The difference I am experimenting with here is to see if swapping out entire modules rather than individual functions is less intrusive on the code under test.
The problem of separating determinism from nondeterminism has been solved for decades with dependency injection,
and until someone is able to point out a better way,
I am sticking with the theory of dependency injection for now.
The idea is to pass modules into the parameter list in production,
and make stubs and fakes look like modules for test.
This turned out to be much more difficult than I expected, but I am not sure if that is because it is not easy in Elixir, or if I am not aware of the proper language constructs.
I ended up dynamically naming my fake and stub modules to keep their state isolated from each other.
I refactored duplication with a macro.
I pasted the IOFake module to the end of test/test_helper.exs because I couldn’t figure out another way to get the other tests to see it.
Although I would prefer a solution that is more simple and less clever,
this does still seem, to me, to be an objectively superior way to test non-determinism in Elixir code than what I have seen so far in the ecosystem.
I can run my tests in parallel without worrying about colliding state.
I have taken complete control of all non-determinism.
The code under test does not need to change much (I could even leave the parameters capitalized, making them look like modules instead of parameters).
I don’t have to make excuses regarding why this or that is too hard to test, I just test everything.
I only did IO.puts here, but you can see how this can be extended to any non-determinism.
For those of you that are interested in this kind of thing, do you see any flaws in my logic?
Or perhaps I missed a better way because I don’t know elixir that well yet?
defmodule ConcurrentA do
def send_line(line, io) do
io.puts(line)
end
end
defmodule ConcurrentB do
def send_line(line, io) do
io.puts(line)
end
end
defmodule ConcurrentATest do
use ExUnit.Case, async: true
test "send line" do
Fake.with_io fn io ->
ConcurrentA.send_line("hello", io)
ConcurrentA.send_line("world", io)
actual = io.get_lines()
expected = ["hello", "world"]
assert expected == actual
end
end
end
defmodule ConcurrentBTest do
use ExUnit.Case, async: true
test "send line" do
Fake.with_io fn io ->
ConcurrentB.send_line("hello", io)
ConcurrentB.send_line("world", io)
actual = io.get_lines()
expected = ["hello", "world"]
assert expected == actual
end
end
end
# Pasted at the end of test_helper.exs
defmodule IOFake do
defmacro __using__(_) do
quote do
def loop(lines) do
receive do
{:get_lines, caller} ->
send(caller, Enum.reverse(lines))
loop(lines)
{:puts, line} ->
new_lines = [line | lines]
loop(new_lines)
{:stop, caller} ->
IO.puts("stopped #{__MODULE__}")
send(caller, :stopped)
x -> raise "unmatched pattern #{inspect x}"
end
end
def puts(line) do
send(__MODULE__, {:puts, line})
end
def get_lines() do
send(__MODULE__, {:get_lines, self()})
receive do x -> x end
end
def start() do
process = spawn_link(fn -> __MODULE__.loop([]) end)
Process.register(process, __MODULE__)
IO.puts("started #{__MODULE__}")
end
def stop() do
send(__MODULE__, {:stop, self()})
receive do x -> x end
end
end
end
end
defmodule Fake do
def with_io(f) do
id = System.unique_integer([:positive])
{_, io, _, _} = defmodule String.to_atom("IOFake#{id}") do
use IOFake
end
io.start()
result = f.(io)
io.stop()
result
end
end
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
JohnnyCurran
If this is what you want to do, I think you should read:
Doing something like:
then in your app config you can:
And then in your app code you can:
If you’re leaning towards passing around side effecting modules to all of your code - I would reconsider
Unless you are talking about testing the actual implementations of
IO,Filethen I can’t be of more help. But if you want to make your tests have deterministic side effects then I think the article will be helpful for youal2o3cr
I can see how it could be extended, but I can also see how it could get out of hand as “bigger” functions end up having to shuffle around a bunch of arguments as their dependencies need different things.
One major challenge with this style: there’s no way for the compiler to verify calls like
io.puts(line)have the right arity (or even refer to a function that exists) so you’re converting a lot of compile-time errors into runtime ones.benwilson512
The conclusion of that thread was to try Mox, which also operates at the Module level as you propose. Can you elaborate as to what you tried with Mox and what issues you found with it?
SeanShubin
My impression of Mox was that it was overly intrusive on the design of the code, and its reliance on global state made it unclear how I could run tests in parallel. If you look at their example here:
You can see the implementation is doing things like “Application.get_env(:my_app, :weather, MyApp.ExternalWeatherAPI)”. While my example does some weird things as well to make the module name dynamic, that weirdness is at least relegated to the test. The code under test, barely needs to change at all. If I really wanted to I could actually only change the signatures, so that IO comes from the parameter list rather than an import. With Mox I felt like I was doing most of the test infrastructure in the code rather than the test.
sodapopcan
The only “global state” it relies on is which implementation of a module to load. Modules themselves have no state so it has no effect on concurrent tests.
ityonemo
Mox lets you do concurrent testing. Each expect is bound to the lifetime of the test, and processes can figure out which test they map to to get where they need to go.
Instead of passing modules around, a better choice is to assign the module to a module attribute so it will be set at compiletime. This is extremely unobtrusive and basically replaces
Modulenamefor@modulename. If you need to use a default impl aways except for when an expect is explicit, you can use stub_with to set it.If you are wondering about the mechanics of how concurrency works with mox, here:
benwilson512
The weirdness in your case is passing in all of the dependencies as arguments, which intrudes into not only ever function in that module, but also every function that calls those functions.
In Mox, the immediate caller of the behavior has to do a lookup of the which behavior is currently in place, but callers thereof are not affected. The “global” bit is simply which behavior implementation is configured. As @ityonemo notes, the specific functions are test specific, they are not global at all.
tfwright
Not to pile on, but as someone who is also passionate about not allowing test specific concerns to leak into the architecture of an app, this seems like the crux of the issue. If the entire goal is to push test specific logic into the tests, then introducing the requirement that each implementation needs to be passed in the arguments seems like a step backward. The application environment seems like the natural place to store information about which modules the application should use in the current environment, especially since one of the challenges in writing elixir is already the need to pass around lots of references in arguments.
D4no0
It is true, the approach of injecting every dependency as argument is bad practice here, however I understand where the author comes from. In languages like c# and java using the config approach like in elixir is not possible or at the very least it will make some nasty code, so there you inject dependencies in every function/class you can, then rely on some dependency injection tools to manage all this mess.
A good article about these 2 different ways of managing dependencies (even though it is not exactly 1:1 on how this works in elixir): https://www.baeldung.com/cs/dependency-injection-vs-service-locator
SeanShubin
The particular problem I am solving for is making sure the shared state among my fakes (analogous to mocks in the article), is sufficiently isolated that the tests can run independently without stepping on each other. If I have one implementation for test, and a different implementation for prod, I still have a problem because the test implementations are going to clobber each other’s state if run concurrently. The article you linked actually points this out “Furthermore, because mocks like the one above change modules globally, they are particularly aggravating in Elixir as changing global values means you can no longer run that part of your test suite concurrently”. Then later in that same article in the “Mocks as locals” section, it suggests instead passing in the dependency as an argument, which is exactly what I am doing in my example. So as far as I can tell, my latest idea of a solution is actually already following this articles advice.