dashbitco / mox

Mocks and explicit contracts in Elixir
1.35k stars 77 forks source link

Mocking modules with methods generated by macro #13

Closed ayrat555 closed 7 years ago

ayrat555 commented 7 years ago

Is it possible to mock methods that are generated by macro?

For example, there is some behaviour

defmodule Client.Behaviour do
  @callback request(map()) :: {:ok, map()} | {:error, map()} | {:error, atom()}
end

and there is some macro that implements this behaviour

defmodule Client.Macro do
  defmacro __using__(_) do
    @behaviour Client.Behaviour

    methods = Client.Methods.methods # a lot of methods with format {binary, atom}

    quote location: :keep, bind_quoted: [methods: methods] do
      @behaviour Client.Behaviour

      methods
      |> Enum.each(fn({original_name, formatted_name}) ->
       def unquote(formatted_name)(params) when is_list(params) do
         send_request(unquote(original_name), params)
       end
      end)

      def send_request(method_name, params) when is_list(params) do
        # params preparation

        request(params)
      end

      def request(params) do
        {:error, :not_implemented}
      end

      defoverridable [request: 1]
    end
  end
end

and then there are a couple of client that implement only request/1 method

  defmodule Client.HttpClient do
    use Client.Macro

    def request(params)
      # http stuff
    end
  end

 defmodule Client.IpcClient do
    use Client.Macro

    def request(params)
      # Unix socket stuff
    end
  end

this clients are used in other modules so I need somehow mock methods generated by Client.Macro. Should I define behaviour for every dynamically generated method (maybe also dynamically)? or there is no other way but to define ad-hoc mock module? Right now I'm using exvcr for all external request but I know it's not very clean.

P.S. Actually there are a lot of apis whose methods are the same except for their names so I think I'm not the only one having problems mocking them.

josevalim commented 7 years ago

One of the guidelines in Elixir is that every time you you generate a function in the user code it should be to abide to some behaviour, so yes, you need behaviours.

Please see discussion in #9.

josevalim commented 7 years ago

Another approach is for you to use atoms instead of defining functions, so you instead of SomeModule.foo, you would do SomeModule.request(:foo) or similar. This way you have a single interface and no longer rely on the function generation.

ayrat555 commented 7 years ago

Thanks, I guess, my approach with dynamic code generation was wrong