Isolating modules with side effects

269 views
Skip to first unread message

Paulo Almeida

unread,
Apr 15, 2014, 5:37:57 AM4/15/14
to elixir-l...@googlegroups.com
Greetings,

What strategies are you using to isolate modules with side-effects and facilitate unit testing?

One approach I've considered is passing the module name as a parameter and in the unit test just mock the module with side-effects (example below). This worked, but maybe there's a better way to accomplish this that doesn't necessarily involve processes and message passing.

defmodule Repository do
  def write_to_database(record) do
    IO.puts "Writing to DB"
  end
end

defmodule Server do

  def process_request(request, repository) do
    request
    |> request_to_record
    |> repository.write_to_database
  end

  defp request_to_record(request) do
  end

end


Thanks in advance for sharing your thoughts.

José Valim

unread,
Apr 15, 2014, 6:10:42 AM4/15/14
to elixir-l...@googlegroups.com
One approach I've considered is passing the module name as a parameter and in the unit test just mock the module with side-effects (example below). This worked, but maybe there's a better way to accomplish this that doesn't necessarily involve processes and message passing.

defmodule Repository do
  def write_to_database(record) do
    IO.puts "Writing to DB"
  end
end

defmodule Server do

  def process_request(request, repository) do
    request
    |> request_to_record
    |> repository.write_to_database
  end

  defp request_to_record(request) do
  end

end

Passing the module as argument is one of the best options indeed. Message passing is one way to emulate side-effects (and agents will definitely make it easier in the future). Another option is, to use the tuple modules API:

defmodule Repository do
  def write_to_database({ val, Repository }) do
    { val + 1, Repository }
  end
end

Using defrecordp would help to make it more readable. I am not a huge fan though and it only works in case you are returning the given argument.

You could also use meck or consider making a simple mock implementation. Given Erlang now provides $handle_undefined_message, the implementation should be really straight-forward.



José Valim
Skype: jv.ptec
Founder and Lead Developer 


Thanks in advance for sharing your thoughts.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Christopher Atkins

unread,
Apr 15, 2014, 12:30:46 PM4/15/14
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
Leaving aside questions of efficacy* in the specific case of the Repository pattern, in mainstream OOP practice, folks tend to do this:
* define a polymorphic interface for Repository
* use a dependency container to supply a concrete implementation at app startup time
* use a mocking library to synthesize a concretion in test and supply the mock to the SUT (via constructor or property)
* use the mocking library to make assertions about interactions with the mock

Assuming we'd actually want to emulate this practice, I would expect to start with:
* defprotocol Repository
* supply the concrete implementation via: defimpl Repository, for: SomeRecord 

It would be nice, assuming the above, to have something like:
test "writes to repo" do
  repo = defmock Repository, for: SomeRecord do
    assert write_to_database(r = SomeRecord[field1: "value"]) do
      is_valid = r.field2 |> String.contains? "est"
      {is_valid, "field2 incorrect, was #{inspect r.field2}"}
    end
  end
  SubjectUnderTest.method_to_test "value", "question"
  repo.verify
end

I don't know of anything like this proposed defmock for Elixir today. Does it exist? If not, is it workable considering protocol dispatch, maps, and record deprecations?

José Valim

unread,
Apr 15, 2014, 1:11:48 PM4/15/14
to elixir-l...@googlegroups.com

Assuming we'd actually want to emulate this practice, I would expect to start with:
* defprotocol Repository
* supply the concrete implementation via: defimpl Repository, for: SomeRecord 

It would be nice, assuming the above, to have something like:
test "writes to repo" do
  repo = defmock Repository, for: SomeRecord do
    assert write_to_database(r = SomeRecord[field1: "value"]) do
      is_valid = r.field2 |> String.contains? "est"
      {is_valid, "field2 incorrect, was #{inspect r.field2}"}
    end
  end
  SubjectUnderTest.method_to_test "value", "question"
  repo.verify
end

Keep in mind that protocols only work with data types. If you don't have an underling record that you would implement the protocol for then it doesn't work. A Behaviour should suffice in those cases though.

In any case, I wouldn't add the protocol if the main reason for it is testing. Functions are polymorphic (in the sense you can pass any input, get any output back) and it is a great way for dependency injection. In more complex cases, simply passing the module name with a mock/dummy implementation work too.
 
I don't know of anything like this proposed defmock for Elixir today. Does it exist? If not, is it workable considering protocol dispatch, maps, and record deprecations?

Sorry, my previous e-mail was triggered too fast.

There is a tool called "meck" in Erlang. Basically the tool works by defining modules and sometimes by replacing a module in your system by a mocked version. I don't like this approach because modules are global which generates possible race condition and conflicts in between tests. Furthermore, as I have just mentioned, in Elixir it is very easy to pass the module you depend on or a function as argument which helps us avoid such global changes.

That said, someone can probably implement a version based on $handled_undefined_function (my previous e-mail was wrong) that does provide the desired functionality without any of the global behaviour. Here is a quick sample:

iex(1)> defmodule T do
...(1)> def unquote(:"$handle_undefined_function")(fun, args) do
...(1)> IO.puts "Got #{fun}: #{inspect args}"
...(1)> end
...(1)> end
{:module, T,
 <<70, 79, 82, 49, 0, 0, 5, 244, 66, 69, 65, 77, 65, 116, 111, 109, 0, 0, 0, 204, 0, 0, 0, 19, 8, 69, 108, 105, 120, 105, 114, 46, 84, 8, 95, 95, 105, 110, 102, 111, 95, 95, 4, 100, 111, 99, 115, 9, 102, 117, ...>>,
 {:"$handle_undefined_function", 2}}
iex(2)> T.foo(:bar, 1)
Got foo: [:bar, 1]
:ok
 
By providing a module that is able to implement all functions, as above, we can just rely on it, instead of defining modules (which is global) for each test case. For example, someone could do:

    m = mock(foo: fn a, b -> a + b end)
    m.foo(1, 2) #=> 3

And that should work as long as as we make mock/1 return { T, reference } and store the mocked calls in a gen server. By automatically storing it in the server, you could also use it to verify if a function has been called or not (avoiding the need for explicit side-effects). Although one could also setup tracing for verifying if something was called or not.

José Valim

unread,
Apr 15, 2014, 1:18:17 PM4/15/14
to elixir-l...@googlegroups.com
Ah, and before I forget, there is a huge disclaimer! I don't recommend anyone using $handle_undefined_function except for very few and rare occasions. I think a mock library is one of those scenarios, but other than that, I would truly avoid it. :)



José Valim
Skype: jv.ptec
Founder and Lead Developer


Christopher Atkins

unread,
Apr 15, 2014, 3:41:39 PM4/15/14
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
Isn't the reason to add the module as an argument to the function as you suggest to make it testable? I would suggest that having to explicitly supply the module as an argument increases coupling at the call site; I'd go so far as to say that the callee is now a leaky abstraction.

What I like about the handle_undefined_function mocking you described is the generality, but it seems to me that we're just trying to achieve late-binding in a few different ways. Ideally, there would be just one way that was equally useful in the application proper and its tests. I know there are perf and maintainability concerns, but it would be nice to be able to opt-in (or perhaps opt-out) of a dynamic dispatch regime.

José Valim

unread,
Apr 15, 2014, 4:05:55 PM4/15/14
to elixir-l...@googlegroups.com

Isn't the reason to add the module as an argument to the function as you suggest to make it testable? I would suggest that having to explicitly supply the module as an argument increases coupling at the call site; I'd go so far as to say that the callee is now a leaky abstraction.

You are right, good call, we are also adding it for testing! Although you can make the module a default argument so you can at least avoid the coupling at the call site.

What I like about the handle_undefined_function mocking you described is the generality, but it seems to me that we're just trying to achieve late-binding in a few different ways. Ideally, there would be just one way that was equally useful in the application proper and its tests. I know there are perf and maintainability concerns, but it would be nice to be able to opt-in (or perhaps opt-out) of a dynamic dispatch regime.

Exactly! We should definitely choose something that does not perform global changes. There are a couple options to explore and we also need to consider how they should work with protocols. 


--

Robert Virding

unread,
Apr 16, 2014, 6:01:09 PM4/16/14
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
José, when you say "pass the module" do you mean pass the module name?

Robert

José Valim

unread,
Apr 17, 2014, 2:03:56 AM4/17/14
to elixir-l...@googlegroups.com
Yes, exactly!


On Thursday, April 17, 2014, Robert Virding <rvir...@gmail.com> wrote:
José, when you say "pass the module" do you mean pass the module name?

Robert

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Christopher Atkins

unread,
Apr 17, 2014, 9:36:48 AM4/17/14
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
Is modules-as-values something that can be done today, i.e. without changing the VM? I seem to recall it being something Joe played with in his Erlang2. ML-style functors would be a very powerful addition.

The parameterized module non-feature of Erlang seems to imply that modules-as-values was not desired or practicable.

On Thursday, April 17, 2014 2:03:56 AM UTC-4, José Valim wrote:
Yes, exactly!

On Thursday, April 17, 2014, Robert Virding <rvir...@gmail.com> wrote:
José, when you say "pass the module" do you mean pass the module name?

Robert

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-talk+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Dave Thomas

unread,
Apr 17, 2014, 11:24:45 AM4/17/14
to elixir-l...@googlegroups.com, José Valim
A module is just it's name.

iex> is_atom Kernel
true

So technically it is passed by reference, isn't it?




To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.

José Valim

unread,
Apr 17, 2014, 3:32:50 PM4/17/14
to elixir-l...@googlegroups.com
Is modules-as-values something that can be done today, i.e. without changing the VM?

As Dave pointed out, we do have first-class modules that can be passing around. Unless you mean something else? :)
 
I seem to recall it being something Joe played with in his Erlang2. ML-style functors would be a very powerful addition.

ML-style functors are definitely interesting and there are a couple ways we can emulate them in Elixir. One way is through macros. Another way, which is actually simpler, is to rely on the fact we can generate modules dynamically. Pseudo-translating the OCaml example from here, we could have something like:

defmodule Set do
  def generate(name, module) do
    defmodule name do
      @functor_arg module

      def empty() do
        []
      end

      def compare(a, b) do
        ...
        @functor_arg.compare(a, b)
        ...
      end
    end
  end
end

And then:

Set.generate(StringSet, OrderedString)

There may be reasons for making this first class however it is hard for me to judge because:

1. I have never used ML-style functors in anger (so seeing use cases for them in my everyday code is a bit hard)

2. We have behaviours and protocols, which although not equivalent to functors, push people to the direction of having a single Set module and possibly making the comparison function dependent on a given argument

Does it make sense?

Christopher Atkins

unread,
Apr 17, 2014, 5:04:43 PM4/17/14
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
Yep, thanks guys, it makes sense. I appreciate the responses and the opportunity to learn and dialogue.

I was just musing about a generalization of behaviours and protocols and mixins. Were it possible to generalize them, and this kind of thing were made first class, maybe some cool things could fall out.

I'm thinking of something like a right-associative infix operator <| that composed modules.

web_server = my_app <| web_framework <| cowboy <| ranch <| gen_tcp
test_web_server = my_app <| defmodule do
  def request do
  ...mock request...
end

micro_svc = MyBizModule <| MyRepo <| gen_kvstore
test_svc = MyBizModule <| defmodule do
  def save(entity) do
    entity |> is_valid? |> assert "bad entity #{inspect entity}"
  end
end
logged_repo_svc = MyBizModule <| LoggingRepo <| MyRepo <| gen_kvstore

Anyway, the main idea is a kind of currying of parameterized modules, allowing partial implementations, shadowing, decorating, mixins, etc. It's not really about functors, specifically, just about having a single way to compose bundles of functions (proper modules and anonymous ones).

I don't have the faintest clue if this is feasible, but having a blessed, singular way of doing this would be a boon IMO. 
Reply all
Reply to author
Forward
0 new messages