SeanShubin
New to elixir, looking for guidance regarding how to test things like IO, File, and System
Whenever I learn a new language, I always make sure I know how to test it by implementing the following “Hello, world!” application.
defmodule HelloApp do
def main() do
time_unit = :microsecond
microseconds_before = System.monotonic_time time_unit
target = System.argv |> 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
I intentionally choose the requirements for this application because they have the most extreme combination of easy-to-write yet hard-to-test that I can think of.
As it is in any language, the trick is to isolate the side effects behind an abstraction of some kind.
With Elixir this seems harder to do than normal, but perhaps that is simply because I don’t know the proper language constructs to hide the side effects behind an abstraction.
I don’t see a way to swap out System, IO, and File with stub versions because they are sitting in a global namespace.
I did look at ExUnit.CaptureIO, but rejected it for 2 reasons.
First reason is that as the capture is happening globally, I become vulnerable to one test affecting another.
Second reason is that CaptureIO is not generalizable to File and System.
Ideally I would like each test to validate the implementation in a sandbox independent of other tests with all the side-effecting modules swapped out.
I was able to get 100% test coverage by inverting the dependencies, so it looks like this may indeed be the right answer.
However, since this is my first ever Elixir program, I want to check with people with more Elixir experience than me to make sure I am on the right track.
What do you think of this solution to getting IO, File, and System under 100% test coverage?
Can you guide me to a better solution?
For the implementation, I invert each side-effecting dependency
defmodule HelloAppInverted1 do
def main(collaborators) do
system = collaborators.system
file = collaborators.file
io = collaborators.io
time_unit = :microsecond
microseconds_before = system.monotonic_time.(time_unit)
target = system.argv.() |> 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
For production, I configure to use the real thing:
defmodule HelloAppEntry1 do
def main() do
system = %{
:monotonic_time => &System.monotonic_time/1,
:argv => &System.argv/0
}
file = %{
:read! => &File.read!/1
}
io = %{
:puts => &IO.puts/1
}
collaborators = %{
:system => system,
:file => file,
:io => io
}
HelloAppInverted1.main(collaborators)
end
end
For testing, I configure to use stubs:
defmodule HelloAppInverted1Test do
use ExUnit.Case
@moduletag timeout: 1_000
test "say hello to world" do
tester = create_tester(
%{
:command_line_arguments => ["configuration.txt"],
:remaining_monotonic_time_values => [1000, 1234],
:file_contents_by_name => %{
"configuration.txt" => "world"
},
:lines_emitted => []
}
)
tester.run.()
assert tester.lines_emitted.() == ["Hello, world!", "Took 234 microseconds"]
end
def create_tester(initial_state) do
state_process = create_process(initial_state)
monotonic_time = fn time_unit ->
send(state_process, {:get_monotonic_time, time_unit, self()})
receive do
x -> x
end
end
argv = fn ->
send(state_process, {:get_argv, self()})
receive do
x -> x
end
end
read! = fn file_name ->
send(state_process, {:get_file_contents, file_name, self()})
receive do
x -> x
end
end
puts = fn output_string ->
send(state_process, {:puts, output_string})
end
lines_emitted = fn ->
send(state_process, {:get_lines_emitted, self()})
receive do
x -> x
end
end
system = %{
:monotonic_time => monotonic_time,
:argv => argv
}
file = %{
:read! => read!
}
io = %{
:puts => puts
}
collaborators = %{
:system => system,
:file => file,
:io => io
}
run = fn ->
HelloAppInverted1.main(collaborators)
end
%{
:run => run,
:lines_emitted => lines_emitted
}
end
def create_process(state) do
spawn_link(fn -> loop(state) end)
end
def consume_monotonic_time(state) do
[time_value | remaining_time_values ] = state.remaining_monotonic_time_values
new_state = Map.replace(state, :remaining_monotonic_time_values, remaining_time_values)
{new_state, time_value}
end
def append_line(state, line) do
new_lines_emitted = [line | state.lines_emitted]
Map.replace(state, :lines_emitted, new_lines_emitted)
end
def loop(state)do
%{
:command_line_arguments => ["configuration.txt"],
:remaining_monotonic_time_values => [1000, 1234],
:file_contents_by_name => %{
"configuration.txt" => "world"
},
:lines_emitted => []
}
receive do
{:get_lines_emitted, caller} ->
send(caller, Enum.reverse(state.lines_emitted))
loop(state)
{:get_monotonic_time, :microsecond, caller} ->
{new_state, monotonic_time_value} = consume_monotonic_time(state)
send(caller, monotonic_time_value)
loop(new_state)
{:get_argv, caller} ->
send(caller, state.command_line_arguments)
loop(state)
{:get_file_contents, file_name, caller} ->
file_contents = state.file_contents_by_name[file_name]
send(caller, file_contents)
loop(state)
{:puts, line} ->
new_state = append_line(state, line)
loop(new_state)
x ->
raise "unmatched pattern #{inspect x}"
end
end
end
- edit, I said production twice, the last snippet was for testing, not production
Trending in Questions
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
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #elixirconf-eu
- #metaprogramming
- #hex










Showing Posts 16 to 7- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
SeanShubin
I think that is a consequence of me being new to Elixir while at the same time having such ambitious goals regarding testability. When I start coding in a programming language that is new to me, I want to get certain foundations nailed down first.
I have found the ability to write code in a pure functional style has very little to do with the language. I can write any program in a language such as Java in 100% pure functional style and Java is not even trying to be a functional programming language. I have also seen code in Haskell, a pure functional programming language, where the side-effecting IO Monad has so permeated the entire code base that I can’t even comprehend what it means to call that code functional.
The reason why I am so focused on precision regarding what it means to be functional, is that I am trying to parse out the justification for when to take a functional vs non-functional approach. Sometimes it is because of language limitations that ruin the aesthetics of certain styles, sometimes it is the way people are used to coding, sometimes there are well established conventions, and I guess that comment was a challenge to see if the only reason the functional approach was not chosen in a particular case is because it doesn’t occur to people that it is even possible. If you tell me I can’t have pure functional, or maybe it doesn’t make sense to strive for pure functional, that may very well be true, I just want to know why.
I am not seeking any more clarification right now though, I got enough suggestions to keep me busy. I am glad you let me know about the history of Elixir being practical and becoming functional as a side effect, not the other way around.
sodapopcan
Hello @SeanShubin and welcome.
Your target audience is a bit unclear here. Your writing style is attracting responses from seasoned Elixir developers and seasoned Elixir developers are well aware they are not working in a purely functional language. Erlang was developed to solve a practical problem and its functional properties emerged as a consequence of that. It’s never been academic. Elixir developers generally favour pure functions for the most part and push all side-effects to a particular boundary which isn’t necessarily the top boundary (depending on how you think about your system).
I’d say go for it if you want. You will generally find people around here encouraging others to code however they see fit. If you look through some prominent open source Elixir projects there are all sorts of different styles.
SeanShubin
My aspirational goal is to get 100% of my Elixir code tested with unit tests alone. Code not run by the Erlang runtime system, such as SQL database queries, while certainly side effecting, is not Elixir code so I am fine with using integration tests for those as long as all of the Elixir code used to transmit the SQL to the database is unit tested.
In this sense, I believe my sample using dependency inversion did achieve 100% pure code with no side effects for every single bit of my Elixir logic, in a way that can be generalized to any kind of side effects. The only class that did not have test coverage was the class that configured which production implementations to use, which is fine by me because that configuration contains no application behavior, just direct pass throughs to the underlying implementations. It is so simple it it could be generated. All of the application behavior is easy to stub out for unit tests, no need for integration tests for Elixir code and no need to skip test coverage on anything.
That said, I am also willing to be pragmatic about my aspirational goal, and am willing to pull back on it in cases where there is good reason and a decent alternative. I don’t want to be writing Elixir code in a style completely alien to the rest of the Elixir community just for the sake of principle.
I am also willing to explore alternative suggestions that can get me close to my aspirational goal through means I might not be able to think of on my own due to my inexperience with Elixir. This is why I am grateful for the suggestions to look into Mox, Ecto, and jjh42/mock. I might have missed these on my own. Once I have had a chance to review all of the solutions, I feel like I can make an informed decision regarding what tradeoffs between functional purity and pragmatism are sensible. I don’t want to presume I have to make a tradeoff, I want to make sure any tradeoffs I make are for well understood and good reasons.
dimitarvp
Sure. We can’t have 100% pure code without side effects however.
Maybe my mind is not on the same frequency as yours – I am just not sure what you are after in practical terms.
SeanShubin
It is not my intention at all to change the meaning of well established words. I am encouraging people to think about the meaning of the words they use, and use them with intention and precision. The meaning of the word “function” is very well established with a precise mathematical definition. The solution I demonstrated was a functional approach according to this already well established definition created a long time ago by many people who had a lot more collective time to think about it than I ever could.
What I see is a lot of is people thinking they are doing “functional” programming, while at the same time getting inputs from a global namespace rather than getting those inputs from the parameters to their function, or thinking I am not doing functional programming because I have some locally scoped mutable state or an imperative style, while definitionally it is still a function because the same inputs always get the same outputs with no observable side effects.
I can’t control your impressions of what I am saying, I can only attempt to be as clear as I possibly can be. I just want to make sure observers of this thread are aware that the particular impression of me wanting to redefine well established terms is a construct of your own mind that has nothing to do with what I was trying to say. I never said or implied that.
dimitarvp
As a guy why was on both sides of the fence, I can’t differentiate between dependency injection in Java and mocking in Elixir.
Yes, the way it’s done is mechanically different but the achieved result is the same.
I’m not certain what exactly is @SeanShubin trying to encourage us to think about, having in mind that both imperative / OOP and FP languages have very good tools to tackle the problem of side effects and misbehaving 3rd party libraries. Even though I’ve carefully read his long comment above, my only impression is that he wants to change the meaning of words that are already well-established.
And yep,
MoxorPatchwill do the job.Mocktoo, I’ve used that successfully as well.SeanShubin
Thanks! I will definitely check it out.
benwilson512
The thing is, for all practical purposes, I already have this. For stubbing out side effects involving interaction with APIs Mox basically is what you’re asking for with DI, and for interacting with the DB you can either do the same thing or you can use Ecto sandboxes. Personally I prefer Ecto sandboxes because unless you’re doing very simple things with a DB the stubs would be pretty complicated to code.
GitHub - jjh42/mock: Mocking library for Elixir language · GitHub uses
meck(an erlang library) to do actual code substitution which seems to be what you’re referring to in your last paragraph.Languages with a strong reliance on DI aren’t my main wheelhouse so I’m sure I could be missing some nuance, but to me Mox seems like it is exactly what you’re looking for.
Sorc96
You’re right, this has been a solved problem for literally decades. Dependency injection is the very obvious answer here. Unfortunately, most functional languages tend to be pretty bad at it.
It doesn’t help that people often don’t realize that there even is a problem (I’ve experienced the same attitude in the Ruby community as well). And those who do end up making various mocking libraries, each with a different set of disadvantages. As much as the FP community likes to complain about design patterns in OOP that result from shortcomings of the language, this is a great example of the same problem happening in FP as well.
On the other hand, the fact that FP languages tend to be bad at DI has resulted in some very interesting research like algebraic effects, which can be used for many other things as well.
SeanShubin
Are you sure? How specifically is the style of inverting dependencies to be passed in as collaborators not solving the problem of getting the Elixir code under 100% test coverage with unit testing alone. I am not saying it is magic, but if there is a way in which this style is not solving the problem I want to know specifically what that is.