antonmi / espec

Elixir Behaviour Driven Development
Other
808 stars 62 forks source link

Dynamic mocking with `:meck.expect/4` #266

Closed janpieper closed 6 years ago

janpieper commented 6 years ago

I tried to use :merge_expects, which is an option that meck provides. It can be used to define different functions to be executed if the arguments matches:

defmodule Example do
  def hello(name), do: name
end
allow(Example).to accept(:hello, fn "John" -> "Hello John" end, [:merge_expects])
allow(Example).to accept(:hello, fn "John" -> "Hello John" end, [:merge_expects])

Example.hello("John") # "Hello John"
Example.hello("Jane") # no function clause matching in anonymous fn/1

The option :merge_expects will be passed to :meck.new/2, but ESpec than calls :meck.expect/3 instead of :meck.expect/4. Okay, ESpec doesn't know the arguments to pass them to :meck.expect/4.

:meck.new(Example, [:merge_expects])

:meck.expect(Example, :foo, ["John"], fn _ -> "Hello John" end)
:meck.expect(Example, :foo, ["Jane"], fn _ -> "Hello Jane" end)

Example.hello("John") # --> "Hello John"
Example.hello("Jane") # --> "Hello Jane"

I know that this can also be achieved by using pattern matching in the anonymous function:

allow(Example).to accept(:hello, fn
  "John" -> "Hello John"
  "Jane" -> "Hello Jane"
end)

Example.hello("John") # "Hello John"
Example.hello("Jane") # "Hello Jane"

But in my case, the arguments are dynamic.

def mock_example(name, retval) do
  allow(Example).to accept(:hello, fn ^name -> retval end)
end

mock_example("John", "Hello John")
mock_example("Jane", "Hello Jane")

Example.hello("John") # "Hello John"
Example.hello("Jane") # "Hello Jane"

Is there a way to achieve this with ESpec? Maybe with some meta programming voodoo or using a macro?

antonmi commented 6 years ago

Hi, @janpieper ! Sorry for the late response! I will investigate soon.

antonmi commented 6 years ago

Ok, @janpieper ! As I understand your goal is to implement the function:

def mock_example(name, retval) do
  allow(Example).to accept(:hello, fn(name) -> retval end)
end

So you can mock :hello function with any pair of input / output Correct me if I'm wrong.

So I have at least two solutions for. First one works if you have a predefined set of input / outputs. You need just define dictionary (Map) with input / output values.

context "simple greetings" do
    def ret_value_map do
      %{"John" => "Hi, John", "Jane" => "Hi, Jane"}
    end

    def mock_example(name, retvalue) do
      allow(Example).to accept(:hello, fn(name) -> Map.get(ret_value_map, name) end)
    end

    before do
      mock_example("John", "Hi, John")
      mock_example("Jane", "Hi, Jane")
    end

    it "greets John and Jane" do
      expect(Example.hello("John")).to eq("Hi, John")
      expect(Example.hello("Jane")).to eq("Hi, Jane")
    end
  end

If you don't know all the values, you can fill in the dictionary on the flight using Agent:

def mock_example(name, retvalue) do
   put_to_agent(name, retvalue)
   allow(Example).to accept(:hello, fn(name) -> get_from_agent(name) end)
end