Giter Club home page Giter Club logo

hammox's Introduction

Hammox

CI Module Version Hex Docs Total Download License Last Updated

Hammox is a library for rigorous unit testing using mocks, explicit behaviours and contract tests. You can use it to ensure both your mocks and implementations fulfill the same contract.

It takes the excellent Mox library and pushes its philosophy to its limits, providing automatic contract tests based on behaviour typespecs while maintaining full compatibility with code already using Mox.

Hammox aims to catch as many contract bugs as possible while providing useful deep stacktraces so they can be easily tracked down and fixed.

Installation

If you are currently using Mox, delete it from your list of dependencies in mix.exs. Then add :hammox:

def deps do
  [
    {:hammox, "~> 0.7", only: :test}
  ]
end

Starting from scratch

Read "Mocks and explicit contracts" by JosΓ© Valim. Then proceed to the Mox documentation. Once you are comfortable with Mox, switch to using Hammox.

Migrating from Mox

Replace all occurrences of Mox with Hammox. Nothing more is required; all your mock calls in test are now ensured to conform to the behaviour typespec.

Example

Typical mock setup

Let's say we have a database which can get us user data. We have a module, RealDatabase (not shown), which implements the following behaviour:

defmodule Database do
  @callback get_users() :: [binary()]
end

We use this client in a Stats module which can aggregate data about users:

defmodule Stats do
  def count_users(database \\ RealDatabase) do
    length(database.get_users())
  end
end

And we create a unit test for it:

defmodule StatsTest do
  use ExUnit.Case, async: true

  test "count_users/0 returns correct user count" do
    assert 2 == Stats.count_users()
  end
end

For this test to work, we would have to start a real instance of the database and provision it with two users. This is of course unnecessary brittleness β€” in a unit test, we only want to test that our Stats code provides correct results given specific inputs. To simplify, we will create a mocked Database using Mox and use it in the test:

defmodule StatsTest do
  use ExUnit.Case, async: true
  import Mox

  test "count_users/0 returns correct user count" do
    defmock(DatabaseMock, for: Database)
    expect(DatabaseMock, :get_users, fn ->
      ["joe", "jim"]
    end)

    assert 2 == Stats.count_users(DatabaseMock)
  end
end

The test now passes as expected.

The contract breaks

Imagine that some time later we want to add error flagging for our database client. We change RealDatabase and the corresponding behaviour, Database, to return an ok/error tuple instead of a raw value:

defmodule Database do
  @callback get_users() :: {:ok, [binary()]} | {:error, term()}
end

However, The Stats.count_users/0 test will still pass, even though the function will break when the real database client is used! This is because the mock is now invalid β€” it no longer implements the given behaviour, and therefore breaks the contract. Even though Mox is supposed to create mocks following explicit contracts, it does not take typespecs into account.

This is where Hammox comes in. Simply replace all occurrences of Mox with Hammox (for example, import Mox becomes import Hammox, etc) and you will now get this when trying to run the test:

** (Hammox.TypeMatchError)
Returned value ["joe", "jim"] does not match type {:ok, [binary()]} | {:error, term()}.

Now the consistency between the mock and its behaviour is enforced.

Completing the triangle

Hammox automatically checks mocks with behaviours, but what about the real implementations? The real goal is to keep all units implementing a given behaviour in sync.

You can decorate any function with Hammox checks by using Hammox.protect/2. It will return an anonymous function which you can use in place of the original module function. An example test:

defmodule RealDatabaseTest do
  use ExUnit.Case, async: true

  test "get_users/0 returns list of users" do
    get_users_0 = Hammox.protect({RealDatabase, :get_users, 0}, Database)
    assert {:ok, ["real-jim", "real-joe"]} == get_users_0.()
  end
end

It's a good idea to put setup logic like this in a setup_all hook and then access the protected functions using the test context:

defmodule RealDatabaseTest do
  use ExUnit.Case, async: true

  setup_all do
    %{get_users_0: Hammox.protect({RealDatabase, :get_users, 0}, Database)}
  end

  test "get_users/0 returns list of users", %{get_users_0: get_users_0} do
    assert {:ok, ["real-jim", "real-joe"]} == get_users_0.()
  end
end

Hammox also provides a setup_all friendly version of Hammox.protect which leverages this pattern. Simply pass both the implementation module and the behaviour module and you will get a map of all callbacks defined by the behaviour as decorated implementation functions.

defmodule RealDatabaseTest do
  use ExUnit.Case, async: true

  setup_all do
    Hammox.protect(RealDatabase, Database)
  end

  test "get_users/0 returns list of users", %{get_users_0: get_users_0} do
    assert {:ok, ["real-jim", "real-joe"]} == get_users_0.()
  end
end

Alternatively, if you're up for trading explicitness for some macro magic, you can use use Hammox.Protect to locally define protected versions of functions you're testing, as if you imported the module:

defmodule RealDatabaseTest do
  use ExUnit.Case, async: true
  use Hammox.Protect, module: RealDatabase, behaviour: Database

  test "get_users/0 returns list of users" do
    assert {:ok, ["real-jim", "real-joe"]} == get_users()
  end
end

Why use Hammox for my application code when I have Dialyzer?

Dialyzer cannot detect Mox style mocks not conforming to typespec.

The main aim of Hammox is to enforce consistency between behaviours, mocks and implementations. This is best achieved when both mocks and implementations are subjected to the exact same checks.

Dialyzer is a static analysis tool; Hammox is a dynamic contract test provider. They operate differently and one can catch some bugs when the other doesn't. While it is true that Hammox would be redundant given a strong, strict, TypeScript-like type system for Elixir, Dialyzer is far from providing that sort of coverage.

Protocol types

A t() type defined on a protocol is taken by Hammox to mean "a struct implementing the given protocol". Therefore, trying to pass :atom for an Enumerable.t() will produce an error, even though the type is defined as term():

** (Hammox.TypeMatchError)
Returned value :atom does not match type Enumerable.t().
  Value :atom does not implement the Enumerable protocol.

Disable protection for specific mocks

Hammox also includes Mox as a dependency. This means that if you would like to disable Hammox protection for a specific mock, you can simply use vanilla Mox for that specific instance. They will interoperate without problems.

Limitations

  • For anonymous function types in typespecs, only the arity is checked. Parameter types and return types are not checked.

Telemetry

Hammox now includes telemetry events! See Telemetry Guide for more information.

License

Copyright 2019 MichaΕ‚ Szewczak

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

hammox's People

Contributors

adamnbowen avatar adzz avatar camilleryr avatar codepr avatar dependabot-preview[bot] avatar dependabot[bot] avatar dkuku avatar florian3k avatar kamilkowalski avatar kianmeng avatar msz avatar saneery avatar sophisticasean avatar vitortrin avatar wojtekmach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

hammox's Issues

Display behaviour and callback in errors

It would be great if hammox reports the behaviour and callback which was used to verify values.
Modules are set in runtime, so it takes time to find the behaviour. This is not critical if you write the code which fails, so you know the context, but the situation is the different when you add a library to the existing project and have to fix a lot of errors.

GenServer.handle_call/3 mock ?

I have been using hammox for a little while, and I recently tried to mock GenServer handle_call/3 callback.

I ve trimmed my problem down to this script, where I attempt to mock GenServer.handle_call/3 to provide various implementation to a genserver while testing, as a mock.
mocktest.exs:

ExUnit.start()

Hammox.defmock(MyMock, for: GenServer)

defmodule GSTest do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(
      __MODULE__,
      {42},
      opts
    )
  end

  def call(srv, req) do
    GenServer.call(srv, req)
  end

  @impl true
  def init(args) do
    {:ok, {"some", "state", args}}
  end

  @impl true
  def handle_call(request, from, state) do
    # transparent forward
    MyMock.handle_call(request, from, state)
  end
end

defmodule GSTest.Test do
  use ExUnit.Case

  import Hammox

  setup do
    proc = start_supervised!({GSTest, name: GSTest.Process})
    %{proc: proc}
  end

  test "lets mock the genserver callback", %{proc: proc} do
    MyMock
    |> allow(self(), proc)
    # replying with the current state
    |> expect(:handle_call, fn (request, from, state) -> {:reply, state, state} end)

    # call and get a response
    {:ok, response} = GenServer.call(proc, {:myrequest, "my_param"})

    # the response is from the mock, returning the actual process state
    assert response == {"some", "state"}
  end
end

when running this (with mix run and the proper dependencies installed), I get the following error

$ mix run mocktest.exs 
warning: variable "from" is unused (if the variable is not meant to be used, prefix it with an underscore)
  mocktest.exs:46: GSTest.Test."test lets mock the genserver callback"/1

warning: variable "request" is unused (if the variable is not meant to be used, prefix it with an underscore)
  mocktest.exs:46: GSTest.Test."test lets mock the genserver callback"/1

[error] GenServer GSTest.Process terminating
** (MatchError) no match of right hand side value: {:type, 518, :bounded_fun, [{:type, 518, :fun, [{:type, 518, :product, [{:ann_type, 518, [{:var, 518, :request}, {:type, 518, :term, []}]}, {:remote_type, 0, [{:atom, 0, GenServer}, {:atom, 0, :from}, []]}, {:ann_type, 518, [{:var, 518, :state}, {:type, 518, :term, []}]}]}, {:type, 520, :union, [{:type, 519, :tuple, [{:atom, 0, :reply}, {:var, 519, :reply}, {:var, 519, :new_state}]}, {:type, 520, :tuple, [{:atom, 0, :reply}, {:var, 520, :reply}, {:var, 520, :new_state}, {:type, 520, :union, [{:type, 520, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 520, :term, []}]}]}]}, {:type, 0, :tuple, [{:atom, 0, :noreply}, {:var, 521, :new_state}]}, {:type, 522, :tuple, [{:atom, 0, :noreply}, {:var, 522, :new_state}, {:type, 522, :union, [{:type, 522, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 522, :term, []}]}]}]}, {:type, 523, :tuple, [{:atom, 0, :stop}, {:var, 523, :reason}, {:var, 523, :reply}, {:var, 523, :new_state}]}, {:type, 524, :tuple, [{:atom, 0, :stop}, {:var, 524, :reason}, {:var, 524, :new_state}]}]}]}, [{:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reply}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :new_state}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reason}, {:type, 525, :term, []}]]}]]}
    (hammox 0.4.0) lib/hammox.ex:466: Hammox.arg_typespec/2
    (hammox 0.4.0) lib/hammox.ex:375: anonymous fn/2 in Hammox.match_args/2
    (elixir 1.11.2) lib/enum.ex:1399: Enum."-map/2-lists^map/1-0-"/2
    (hammox 0.4.0) lib/hammox.ex:374: Hammox.match_args/2
    (hammox 0.4.0) lib/hammox.ex:361: Hammox.match_call/3
    (hammox 0.4.0) lib/hammox.ex:346: anonymous fn/4 in Hammox.check_call/3
    (elixir 1.11.2) lib/enum.ex:3764: Enumerable.List.reduce/3
    (elixir 1.11.2) lib/enum.ex:2231: Enum.reduce_while/3
    (hammox 0.4.0) lib/hammox.ex:345: Hammox.check_call/3
    (hammox 0.4.0) lib/hammox.ex:338: Hammox.protected_code/3
    (stdlib 3.13.2) gen_server.erl:706: :gen_server.try_handle_call/4
    (stdlib 3.13.2) gen_server.erl:735: :gen_server.handle_msg/6
    (stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.513.0>): {:myrequest, "my_param"}
State: {"some", "state", {42}}
Client #PID<0.513.0> is alive

    (stdlib 3.13.2) gen.erl:208: :gen.do_call/4
    (elixir 1.11.2) lib/gen_server.ex:1024: GenServer.call/3
    mocktest.exs:49: GSTest.Test."test lets mock the genserver callback"/1
    (ex_unit 1.11.2) lib/ex_unit/runner.ex:391: ExUnit.Runner.exec_test/1
    (stdlib 3.13.2) timer.erl:166: :timer.tc/1
    (ex_unit 1.11.2) lib/ex_unit/runner.ex:342: anonymous fn/4 in ExUnit.Runner.spawn_test_monitor/4


  1) test lets mock the genserver callback (GSTest.Test)
     mocktest.exs:42
     ** (exit) exited in: GenServer.call(#PID<0.515.0>, {:myrequest, "my_param"}, 5000)
         ** (EXIT) an exception was raised:
             ** (MatchError) no match of right hand side value: {:type, 518, :bounded_fun, [{:type, 518, :fun, [{:type, 518, :product, [{:ann_type, 518, [{:var, 518, :request}, {:type, 518, :term, []}]}, {:remote_type, 0, [{:atom, 0, GenServer}, {:atom, 0, :from}, []]}, {:ann_type, 518, [{:var, 518, :state}, {:type, 518, :term, []}]}]}, {:type, 520, :union, [{:type, 519, :tuple, [{:atom, 0, :reply}, {:var, 519, :reply}, {:var, 519, :new_state}]}, {:type, 520, :tuple, [{:atom, 0, :reply}, {:var, 520, :reply}, {:var, 520, :new_state}, {:type, 520, :union, [{:type, 520, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 520, :term, []}]}]}]}, {:type, 0, :tuple, [{:atom, 0, :noreply}, {:var, 521, :new_state}]}, {:type, 522, :tuple, [{:atom, 0, :noreply}, {:var, 522, :new_state}, {:type, 522, :union, [{:type, 522, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 522, :term, []}]}]}]}, {:type, 523, :tuple, [{:atom, 0, :stop}, {:var, 523, :reason}, {:var, 523, :reply}, {:var, 523, :new_state}]}, {:type, 524, :tuple, [{:atom, 0, :stop}, {:var, 524, :reason}, {:var, 524, :new_state}]}]}]}, [{:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reply}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :new_state}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reason}, {:type, 525, :term, []}]]}]]}
                 (hammox 0.4.0) lib/hammox.ex:466: Hammox.arg_typespec/2
                 (hammox 0.4.0) lib/hammox.ex:375: anonymous fn/2 in Hammox.match_args/2
                 (elixir 1.11.2) lib/enum.ex:1399: Enum."-map/2-lists^map/1-0-"/2
                 (hammox 0.4.0) lib/hammox.ex:374: Hammox.match_args/2
                 (hammox 0.4.0) lib/hammox.ex:361: Hammox.match_call/3
                 (hammox 0.4.0) lib/hammox.ex:346: anonymous fn/4 in Hammox.check_call/3
                 (elixir 1.11.2) lib/enum.ex:3764: Enumerable.List.reduce/3
                 (elixir 1.11.2) lib/enum.ex:2231: Enum.reduce_while/3
                 (hammox 0.4.0) lib/hammox.ex:345: Hammox.check_call/3
                 (hammox 0.4.0) lib/hammox.ex:338: Hammox.protected_code/3
                 (stdlib 3.13.2) gen_server.erl:706: :gen_server.try_handle_call/4
                 (stdlib 3.13.2) gen_server.erl:735: :gen_server.handle_msg/6
                 (stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
     code: {:ok, response} = GenServer.call(proc, {:myrequest, "my_param"})
     stacktrace:
       (elixir 1.11.2) lib/gen_server.ex:1027: GenServer.call/3
       mocktest.exs:49: (test)



Finished in 0.09 seconds (0.08s on load, 0.01s on tests)
1 test, 1 failure

Randomized with seed 249050
[os_mon] memory supervisor port (memsup): Erlang has closed
[os_mon] cpu supervisor port (cpu_sup): Erlang has closed

This is with

$ mix -v
Erlang/OTP 23 [erts-11.1.3] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [hipe]

Mix 1.11.2 (compiled with Erlang/OTP 23)

So it seems to me that somehow hammox cannot handle GenServer.handle_call typespec ?

I am aware of a simple workaround, which is to have a separate behaviour describing what the handle_call will do, and mock that instead. Which is what I usually do, to keep my GenServers light.

Unfortunately in my actual usecase (adding a more generic behavior for many servers in various apps), I need to mock the same signature as GenServer.handle_call/3, so that workaround wouldn't apply.

So, is there a way to mock GenServer.handle_call/3 ? Am I missing something ?
And Thanks for Hammox!

`CaseClauseError` when Displaying Complex Types

Ran into the following bug when a "complex" (see below) type was being rendered in Hammox.

The "complex" type in question is a parameter defined type:

@type car(wheel_type) :: %{
  # ...
  :wheels => [
    wheel_type,
    ...
  ],
  # ...
}

This resulted in the following error in Hammox:

** (CaseClauseError) no case clause matching: ["foo() ::\n  Garage.car(\n    Wheels.chrome_rims()\n  )"

Which occurred with the following stack trace:

(hammox 0.5.0) lib/hammox/type_match_error.ex:157: Hammox.TypeMatchError.type_to_string/1
(hammox 0.5.0) lib/hammox/type_match_error.ex:20: Hammox.TypeMatchError.human_reason/1
(hammox 0.5.0) lib/hammox/type_match_error.ex:123: anonymous fn/1 in Hammox.TypeMatchError.message_string/1
(elixir 1.13.1) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
(hammox 0.5.0) lib/hammox/type_match_error.ex:121: Hammox.TypeMatchError.message_string/1
(hammox 0.5.0) lib/hammox/type_match_error.ex:13: Hammox.TypeMatchError.exception/1
(hammox 0.5.0) lib/hammox.ex:355: Hammox.check_call/3
(hammox 0.5.0) lib/hammox.ex:338: Hammox.protected_code/3

"Cannot load module" error gets swallowed

In a complex type, when there is a module that cannot be loaded, and the types are similar (the type match stacks are the same height) the error is instead that it couldn't match the last element of the union. Possible working example:

defmodule Module1 do
  @type t :: :module1
end

defmodule Module3 do
  @type t :: :module3
end

defmodule Foo do
  @callback foo() :: {:ok, Module1.t()} | {:ok, Module2.t()} | {:ok, Module3.t()}
  def foo, do: {:ok, :nothing}
end

Expected kind of error from calling protected Foo.foo/0: "Could not load Module2"
Actual kind of error from calling protected Foo.foo/0: "Could not match Module3"

CaseClauseError with return type of Ecto.Changeset.t()

I'm using the latest commit on the master branch.

I have a function in a behaviour that returns an Ecto Changeset. If I attempt to wrap this function with Hammox.protect while testing it, I get the following error message and stack trace. I've reproduced this error in a separate repo that I can publish somewhere if it would be helpful; important extracts are below.

I'm happy to try to help track this down, but I'm not sure of the best way to do that. Tips welcome!

     ** (CaseClauseError) no case clause matching: [{:type, 441, :map_field_exact, [{:atom, 0, :__struct__}, {:type, 441, :atom, []}]}]
     stacktrace:
       (hammox 0.2.5) lib/hammox/type_engine.ex:346: Hammox.TypeEngine.match_type/2
       (hammox 0.2.5) lib/hammox/type_engine.ex:12: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir 1.10.0) lib/enum.ex:3686: Enumerable.List.reduce/3
       (elixir 1.10.0) lib/enum.ex:2161: Enum.reduce_while/3
       (hammox 0.2.5) lib/hammox/type_engine.ex:11: Hammox.TypeEngine.match_type/2
       (hammox 0.2.5) lib/hammox/type_engine.ex:12: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir 1.10.0) lib/enum.ex:3686: Enumerable.List.reduce/3
       (elixir 1.10.0) lib/enum.ex:2161: Enum.reduce_while/3
       (hammox 0.2.5) lib/hammox/type_engine.ex:11: Hammox.TypeEngine.match_type/2
       (hammox 0.2.5) lib/hammox/type_engine.ex:375: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir 1.10.0) lib/enum.ex:1400: anonymous fn/3 in Enum.map/2
       (stdlib 3.11.1) maps.erl:232: :maps.fold_1/3
       (elixir 1.10.0) lib/enum.ex:2127: Enum.map/2
       (hammox 0.2.5) lib/hammox/type_engine.ex:374: anonymous fn/2 in Hammox.TypeEngine.match_type/2
       (elixir 1.10.0) lib/enum.ex:3686: Enumerable.List.reduce/3
       (elixir 1.10.0) lib/enum.ex:2161: Enum.reduce_while/3
       (hammox 0.2.5) lib/hammox/type_engine.ex:372: Hammox.TypeEngine.match_type/2
       (hammox 0.2.5) lib/hammox.ex:359: Hammox.match_return_value/2
       (hammox 0.2.5) lib/hammox.ex:327: Hammox.match_call/3
       (hammox 0.2.5) lib/hammox.ex:311: anonymous fn/4 in Hammox.check_call/3

Here are the behavior and implementation:

defmodule HammoxTest.Friends.Impl do
  alias HammoxTest.Friends.Person

  @callback change_person(Person.t(), map()) :: Ecto.Changeset.t()
end

defmodule HammoxTest.Friends do
  alias HammoxTest.Friends.{Impl, Person}

  @behaviour Impl

  @impl Impl
  def change_person(%Person{} = person, attrs \\ %{}) do
    person
    |> Person.changeset(attrs)
  end
end

defmodule HammoxTest.Friends.Person do
  use Ecto.Schema

  @type t :: %__MODULE__{
          first_name: String.t() | nil,
          last_name: String.t() | nil,
          age: integer() | nil
        }

  schema "people" do
    field(:first_name, :string)
    field(:last_name, :string)
    field(:age, :integer)
  end

  def changeset(person, attrs \\ %{}) do
    person
    |> Ecto.Changeset.cast(attrs, [:first_name, :last_name, :age])
    |> Ecto.Changeset.validate_required([:first_name, :last_name])
  end
end

And here is the test:

defmodule HammoxTest.FriendsTest do
  use ExUnit.Case

  import Hammox

  alias HammoxTest.Friends
  alias HammoxTest.Friends.{Impl, Person}

  test "can I type-check a changeset?" do
    change_person_2 = protect({Friends, :change_person, 2}, Impl)

    changeset = change_person_2.(%Person{}, %{age: 42})

    assert changeset.valid?
  end
end

Hammox slow with stripity_stripe lib

When using Hammox.expect returning a struct from stripity_stripe, like the Stripe.Customer struct, it becomes too slow (more or less 5 seconds for each expect call).

Seems to be an issue with the typespec checking (after returning the value), because if I use Hammox.stub or even use Mox.expect, it runs normally.

An example of the call I'am doing:

Hammox.expect(StripeMock, :get_customer, fn _ -> 
  %Stripe,Customer{
      id: "",
      object: "customer",
      address: nil,
      balance: 0,
      created: 1_599_741_039,
      currency: "usd",
      default_source: "",
      deleted: nil,
      delinquent: false,
      description: nil,
      discount: nil,
      email: nil,
      invoice_prefix: "",
      invoice_settings: nil,
      livemode: false,
      metadata: %{},
      name: "",
      next_invoice_sequence: 1,
      payment_method: nil,
      phone: "",
      preferred_locales: [],
      shipping: nil,
      sources: %List{data: [], has_more: false, object: "", total_count: 0, url: ""},
      subscriptions: %List{data: [], has_more: false, object: "", total_count: 0, url: ""},
      tax_exempt: nil,
      tax_ids: %List{data: [], has_more: false, object: "", total_count: 0, url: ""},
    }
end)

Using

  • Erlang/OTP 23
  • Elixir 1.10.4
  • Hammox 0.2.5
  • stripity_stripe: 2.9

ensure_compiled! issue when using Hammox.Protect

When using use Hammox.Protect (and protect/2) with older versions of Elixir (< 1.12), the following error is raised because Code.ensure_compiled! isn't available in the check_module_exists util function.

** (UndefinedFunctionError) function Code.ensure_compiled!/1 is undefined or private. Did you mean one of:

      * ensure_compiled/1
      * ensure_loaded/1

    (elixir 1.11.4) Code.ensure_compiled!(Hui.ResponseParsers.Parser)
    lib/hammox/protect.ex:153: Hammox.Protect.get_funs!/1

A fix should be created in similar vein to Mox:
https://github.com/dashbitco/mox/blob/a59a1840676ceb0ef6c3e0f4959242ccb5d593ab/lib/mox.ex#L794-L809

Happy to submit a PR since Hammox is still elixir: "~> 1.7".

obscure error when module to `protect` does not exist

** (MatchError) no match of right hand side value: :error
     stacktrace:
       (hammox) lib/hammox.ex:336: Hammox.fetch_typespecs/3
       (hammox) lib/hammox.ex:322: Hammox.fetch_typespecs!/3
       (hammox) lib/hammox.ex:121: Hammox.protect/2
       (hammox) lib/hammox.ex:178: anonymous fn/4 in Hammox.protect/3
       (elixir) lib/enum.ex:1336: Enum."-map/2-lists^map/1-0-"/2
       (elixir) lib/enum.ex:2994: Enum.flat_map_list/2
       (hammox) lib/hammox.ex:168: Hammox.protect/3

Should show a good explanation what happened instead.

Type guards in specs not supported

It is possible to define guards in function typespecs. However, trying to use Hammox with such typespecs will cause a MatchError involving a :bounded_fun type tuple similar to this one:

 ** (MatchError) no match of right hand side value: {:type, 518, :bounded_fun, [{:type, 518, :fun, [{:type, 518, :product, [{:ann_type, 518, [{:var, 518, :request}, {:type, 518, :term, []}]}, {:remote_type, 0, [{:atom, 0, GenServer}, {:atom, 0, :from}, []]}, {:ann_type, 518, [{:var, 518, :state}, {:type, 518, :term, []}]}]}, {:type, 520, :union, [{:type, 519, :tuple, [{:atom, 0, :reply}, {:var, 519, :reply}, {:var, 519, :new_state}]}, {:type, 520, :tuple, [{:atom, 0, :reply}, {:var, 520, :reply}, {:var, 520, :new_state}, {:type, 520, :union, [{:type, 520, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 520, :term, []}]}]}]}, {:type, 0, :tuple, [{:atom, 0, :noreply}, {:var, 521, :new_state}]}, {:type, 522, :tuple, [{:atom, 0, :noreply}, {:var, 522, :new_state}, {:type, 522, :union, [{:type, 522, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 522, :term, []}]}]}]}, {:type, 523, :tuple, [{:atom, 0, :stop}, {:var, 523, :reason}, {:var, 523, :reply}, {:var, 523, :new_state}]}, {:type, 524, :tuple, [{:atom, 0, :stop}, {:var, 524, :reason}, {:var, 524, :new_state}]}]}]}, [{:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reply}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :new_state}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reason}, {:type, 525, :term, []}]]}]]}
                 (hammox 0.4.0) lib/hammox.ex:466: Hammox.arg_typespec/2
                 (hammox 0.4.0) lib/hammox.ex:375: anonymous fn/2 in Hammox.match_args/2
                 (elixir 1.11.2) lib/enum.ex:1399: Enum."-map/2-lists^map/1-0-"/2
                 (hammox 0.4.0) lib/hammox.ex:374: Hammox.match_args/2
                 (hammox 0.4.0) lib/hammox.ex:361: Hammox.match_call/3
                 (hammox 0.4.0) lib/hammox.ex:346: anonymous fn/4 in Hammox.check_call/3
                 (elixir 1.11.2) lib/enum.ex:3764: Enumerable.List.reduce/3
                 (elixir 1.11.2) lib/enum.ex:2231: Enum.reduce_while/3
                 (hammox 0.4.0) lib/hammox.ex:345: Hammox.check_call/3
                 (hammox 0.4.0) lib/hammox.ex:338: Hammox.protected_code/3
                 (stdlib 3.13.2) gen_server.erl:706: :gen_server.try_handle_call/4
                 (stdlib 3.13.2) gen_server.erl:735: :gen_server.handle_msg/6
                 (stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

This is a public feature of Elixir typespecs and needs to be supported.

FunctionClauseError for a parameterized type referencing another parameterized type

Should Hammox support parameterized union type consisting of custom types?

defmodule Result do
  @type ok(any) :: {:ok, any}
  @type error(any) :: {:error, any}

  @typedoc "the two-track type"
  @type t(a, b) :: ok(a) | error(b)
end

I get the following error:

     ** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2

     The following arguments were given to Hammox.TypeEngine.match_type/2:

         # 1
         :foo

         # 2
         {:var, 7, :a}

     Attempted function clauses (showing 10 out of 91):

         def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
         def match_type(_value, {:type, _, :any, []})
         def match_type(value, {:type, _, :none, []} = type)
         def match_type(value, {:type, _, :atom, []}) when is_atom(value)
         def match_type(value, {:type, _, :atom, []} = type)
         def match_type(value, {:type, _, :map, :any}) when is_map(value)
         def match_type(value, {:type, _, :pid, []}) when is_pid(value)
         def match_type(value, {:type, _, :pid, []} = type)
         def match_type(value, {:type, _, :port, []}) when is_port(value)
         def match_type(value, {:type, _, :port, []} = type)

     code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
     stacktrace:
       (hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox/type_engine.ex:100: anonymous fn/1 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:2984: Enum.find_value_list/3
       (hammox) lib/hammox/type_engine.ex:99: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox/type_engine.ex:9: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3
       (hammox) lib/hammox/type_engine.ex:8: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox.ex:312: Hammox.match_return_value/2
       (hammox) lib/hammox.ex:280: Hammox.match_call/3
       (hammox) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
       (elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3
       (hammox) lib/hammox.ex:263: Hammox.check_call/3
       (hammox) lib/hammox.ex:256: Hammox.protected_code/3
       test/hammox_playground_test.exs:13: (test)

1.0 release

is there a plan to release 1.0 for semver stability ?

respect opaqueness of `@opaque` types

Right now Hammox is treating @opaque just like a public type, which means if matching fails it will show a stacktrace going into the internals of the type. We should modify this so that the stacktrace stops at the opaque type.

FunctionClauseError raised on return type annotation

For example, the following type definition works:

  @type greeting :: String.t()
  @callback perform(Decimal.t()) :: greeting

but the following raises a FunctionCaluseError exception:

  @callback perform(Decimal.t()) :: greeting :: String.t()

Stacktrace:

     ** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2

     The following arguments were given to Hammox.TypeEngine.match_type/2:

         # 1
         "hello"

         # 2
         {:ann_type, 7, [{:var, 7, :greeting}, {:remote_type, 7, [{:atom, 0, String}, {:atom, 0, :t}, []]}]}

     Attempted function clauses (showing 10 out of 91):

         def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
         def match_type(_value, {:type, _, :any, []})
         def match_type(value, {:type, _, :none, []} = type)
         def match_type(value, {:type, _, :atom, []}) when is_atom(value)
         def match_type(value, {:type, _, :atom, []} = type)
         def match_type(value, {:type, _, :map, :any}) when is_map(value)
         def match_type(value, {:type, _, :pid, []}) when is_pid(value)
         def match_type(value, {:type, _, :pid, []} = type)
         def match_type(value, {:type, _, :port, []}) when is_port(value)
         def match_type(value, {:type, _, :port, []} = type)

     code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
     stacktrace:
       (hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox.ex:312: Hammox.match_return_value/2
       (hammox) lib/hammox.ex:280: Hammox.match_call/3
       (hammox) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
       (elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3
       (hammox) lib/hammox.ex:263: Hammox.check_call/3
       (hammox) lib/hammox.ex:256: Hammox.protected_code/3
       test/hammox_playground_test.exs:13: (test)

Errors on Parametrized Types

From Ecto.Changeset, there is a t() defined such as:

  @type t(data_type) :: %Changeset{valid?: boolean(),
                        ...
                        data: data_type,
                        ...
                        types: nil | %{atom => Ecto.Type.t}}

In our behaviour, we have something like:

  @type user_changeset :: Ecto.Changeset.t(User.t())

  @callback password_operation(user_changeset) :: user_changeset
  @callback password_operation(user_changeset, attrs :: map) :: user_changeset

In our implementation:

  @type user_changeset :: Ecto.Changeset.t(User.t())
  @impl CredentialsHelper
  @spec password_operation(user_changeset) :: user_changeset
  @spec password_operation(user_changeset, map) :: user_changeset
  def password_operation(cs, attrs \\ %{}),
    do: ...

When trying to stub or expect, Hammox raises:

     ** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2

     The following arguments were given to Hammox.TypeEngine.match_type/2:
     
         # 1
         nil
     
         # 2
         {:var, 282, :data_type}

Note: we tried every possible combination of typing here - directly Ecto.Changeset.t, Ecto.Changeset.t(any) etc. Only replacing the changeset type by any on the behaviour seems to fix the problem.

FunctionClauseError with Ecto.Changeset.t()

A behaviour with a spec containing the type Ecto.Changeset.t() will raise a FunctionClauseError:

     ** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2

     The following arguments were given to Hammox.TypeEngine.match_type/2:

         # 1
         nil

         # 2
         {:var, 294, :data_type}

     Attempted function clauses (showing 10 out of 91):

         def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
         def match_type(_value, {:type, _, :any, []})
         def match_type(value, {:type, _, :none, []} = type)
         def match_type(value, {:type, _, :atom, []}) when is_atom(value)
         def match_type(value, {:type, _, :atom, []} = type)
         def match_type(value, {:type, _, :map, :any}) when is_map(value)
         def match_type(value, {:type, _, :pid, []}) when is_pid(value)
         def match_type(value, {:type, _, :pid, []} = type)
         def match_type(value, {:type, _, :port, []}) when is_port(value)
         def match_type(value, {:type, _, :port, []} = type)

     code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
     stacktrace:
       (hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox/type_engine.ex:372: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:1340: anonymous fn/3 in Enum.map/2
       (stdlib) maps.erl:232: :maps.fold_1/3
       (elixir) lib/enum.ex:1964: Enum.map/2
       (hammox) lib/hammox/type_engine.ex:371: anonymous fn/2 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:3358: Enumerable.Map.reduce_list/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3
       (hammox) lib/hammox/type_engine.ex:369: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox.ex:312: Hammox.match_return_value/2
       (hammox) lib/hammox.ex:280: Hammox.match_call/3
       (hammox) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
       (elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3
       (hammox) lib/hammox.ex:263: Hammox.check_call/3
       (hammox) lib/hammox.ex:256: Hammox.protected_code/3
       test/hammox_playground_test.exs:13: (test)

add a builtin Hammox.enumerable/1 type

Add an enumerable type which can be parametrized by element type. This type will be checked for implementing the Enumerable protocol and then each element of the enumerable will be checked with element type.

Add a warning that this will consume the enumerable at type check time if the enumerable is lazy or has side effects.

support all Elixir versions since 1.5

This is to make it actually backwards compatible with Mox which requires Elixir ~1.5.

Areas to look at:

  • removing @doc keyword tags
  • looking into copying code from Code.Typespec directly into the project

Hammox breaks when there is a @typep

This code in type_engine.ex accepts only @type:

  defp get_type(type_list, type_name, arity) do
    case Enum.find(type_list, fn {:type, {name, _type, params}} ->
           name == type_name and length(params) == arity
         end) do
      nil -> {:error, {:type_not_found, {type_name, arity}}}
      type -> {:ok, type}
    end
  end

However, Typespecs accepts @type, @typep and @opaque (source). This causes hammox to break:

     ** (FunctionClauseError) no function clause matching in anonymous fn/1 in Hammox.TypeEngine.get_type/3

     The following arguments were given to anonymous fn/1 in Hammox.TypeEngine.get_type/3:

         # 1
         {:typep, {:names, {:remote_type, 133, [{:atom, 0, MapSet}, {:atom, 0, :t}, []]}, []}}

     stacktrace:
       (hammox 0.2.4) lib/hammox/type_engine.ex:695: anonymous fn/1 in Hammox.TypeEngine.get_type/3
       (elixir 1.10.2) lib/enum.ex:3305: Enum.find_list/3
       (hammox 0.2.4) lib/hammox/type_engine.ex:695: Hammox.TypeEngine.get_type/3
       (hammox 0.2.4) lib/hammox/type_engine.ex:656: Hammox.TypeEngine.resolve_remote_type/1
       (hammox 0.2.4) lib/hammox/type_engine.ex:621: Hammox.TypeEngine.match_type/2
       (hammox 0.2.4) lib/hammox.ex:312: Hammox.match_return_value/2
       (hammox 0.2.4) lib/hammox.ex:280: Hammox.match_call/3
       (hammox 0.2.4) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
       (elixir 1.10.2) lib/enum.ex:3686: Enumerable.List.reduce/3
       (elixir 1.10.2) lib/enum.ex:2161: Enum.reduce_while/3
       (hammox 0.2.4) lib/hammox.ex:263: Hammox.check_call/3
       (hammox 0.2.4) lib/hammox.ex:256: Hammox.protected_code/3

In my case, I'm using Ecto.Multi.t() as the return value of a function, and this structure defines @typep: https://github.com/elixir-ecto/ecto/blob/v3.4.4/lib/ecto/multi.ex#L133

Bug with Hammox when upgrading Elixir

Hello,

I am upgrading from elixir 1.15 to 1.16.3, OTP version is the same 26.2.5

yet test that were passing in 1.15 are now failing with this error.

any idea what the issue might be?

** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2

 The following arguments were given to Hammox.TypeEngine.match_type/2:

     # 1
     %{}

     # 2
     {:type, {87, 25}, :record, [{:atom, {87, 26}, :set}, {:type, {87, 30}, :field_type, [{:atom, {87, 30}, :segs}, {:remote_type, 0, [{:atom, 0, :sets}, {:atom, 0, :segs}, [{:type, {57, 16}, :term, []}]]}]}]}

 Attempted function clauses (showing 10 out of 93):

     def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
     def match_type(_value, {:type, _, :any, []})
     def match_type(value, {:type, _, :none, []} = type)
     def match_type(value, {:type, _, :atom, []}) when is_atom(value)
     def match_type(value, {:type, _, :atom, []} = type)
     def match_type(value, {:type, _, :map, :any}) when is_map(value)
     def match_type(value, {:type, _, :pid, []}) when is_pid(value)
     def match_type(value, {:type, _, :pid, []} = type)
     def match_type(value, {:type, _, :port, []}) when is_port(value)
     def match_type(value, {:type, _, :port, []} = type)
     ...
     (83 clauses not shown)

Add support for annotated types in tuples

this doesn't work

  @type event :: {event_name :: atom(), topics :: [String.t()]}

but this works

  @type event_name :: atom()
  @type topics :: [String.t()]
  @type event :: {event_name(), topics()}

Exception thrown with Decimal.t

Could you please tell me if hammox has support for custom types? For example:

defmodule MyType do
  defstruct [:value]

  @type t :: %__MODULE__{
    value: number()
   }
end

Looking through the source code and the following error, I am assuming it doesn't currently:

     The following arguments were given to Hammox.TypeEngine.match_type/2:

         # 1
         1000

         # 2
         {:op, 93, :-, {:integer, 93, 1}}

     Attempted function clauses (showing 10 out of 89):

         def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
         def match_type(_value, {:type, _, :any, []})
         def match_type(value, {:type, _, :none, []} = type)
         def match_type(value, {:type, _, :atom, []}) when is_atom(value)
         def match_type(value, {:type, _, :atom, []} = type)
         def match_type(value, {:type, _, :map, :any}) when is_map(value)
         def match_type(value, {:type, _, :pid, []}) when is_pid(value)
         def match_type(value, {:type, _, :pid, []} = type)
         def match_type(value, {:type, _, :port, []}) when is_port(value)
         def match_type(value, {:type, _, :port, []} = type)

     stacktrace:
       (hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox/type_engine.ex:9: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3
       (hammox) lib/hammox/type_engine.ex:8: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox/type_engine.ex:364: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:1340: anonymous fn/3 in Enum.map/2
       (stdlib) maps.erl:232: :maps.fold_1/3
       (elixir) lib/enum.ex:1964: Enum.map/2
       (hammox) lib/hammox/type_engine.ex:363: anonymous fn/2 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:3358: Enumerable.Map.reduce_list/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3
       (hammox) lib/hammox/type_engine.ex:361: Hammox.TypeEngine.match_type/2
       (hammox) lib/hammox/type_engine.ex:364: anonymous fn/3 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:1340: anonymous fn/3 in Enum.map/2
       (stdlib) maps.erl:232: :maps.fold_1/3
       (elixir) lib/enum.ex:1964: Enum.map/2
       (hammox) lib/hammox/type_engine.ex:363: anonymous fn/2 in Hammox.TypeEngine.match_type/2
       (elixir) lib/enum.ex:3358: Enumerable.Map.reduce_list/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3

Thank you for your help!

match_type missing head for bool?

Hey. I just changed Mox to Hammox in one file and I have lots of these errors:
I'm not sure if there should be a head for that. Just leaving to check if it's a bug

The following arguments were given to Hammox.TypeEngine.match_type/2:
       # 1
      true
      # 2
      {:type, 25, :bool, []}

[idea] add a mix task to setup an area

The library is very useful. But I think that there is a lot of boilerplate to write when setting a new module/area. You need the behaviour, implementation, mock and changes to config - it would be cool to have an official generator for that does the boilerplate for you.

Parameterized type from another module conflicts with local custom type

The following code:

defmodule Result do
  @type ok(any) :: {:ok, any}
end

defmodule HammoxPlayground do
  @type t :: String.t()

  @callback perform(Decimal.t()) :: Result.ok(t())
end

Results in the following TypeMatchError for the correct types:

     ** (Hammox.TypeMatchError)
     Returned value {:ok, "hello"} does not match type Result.ok(t()).
       2nd tuple element "hello" does not match 2nd element type Result.t().
         Could not find type t/0 in Result.
     code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
     stacktrace:
       (hammox) lib/hammox.ex:273: Hammox.check_call/3
       (hammox) lib/hammox.ex:256: Hammox.protected_code/3
       test/hammox_playground_test.exs:13: (test)

As t() is not qualified in Result.ok(t()) Hammox interprets the type as Result.t(), but it is HammoxPlayground .t(). Change to Result.ok(__MODULE__.t()) works, but it seems that it should work unqualified.

Support erlang records

Some Elixir libraries have references to erlang types, like Erlang records.

For example, the OpenTelemetry library has start_opts type:

https://hexdocs.pm/opentelemetry_api/OpenTelemetry.Tracer.html#t:start_opts/0

If you try to reference it and use Hammox protect, it raises an exception like this:

    The following arguments were given to Hammox.TypeEngine.match_type/2:
         # 1
         {:some_attribute, "somevallue"}
         # 2
         {:type, 116, :record, [{:atom, 116, :link}]}
     Attempted function clauses (showing 10 out of 92):
         def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
         def match_type(_value, {:type, _, :any, []})
         def match_type(value, {:type, _, :none, []} = type)
         def match_type(value, {:type, _, :atom, []}) when is_atom(value)
         def match_type(value, {:type, _, :atom, []} = type)
         def match_type(value, {:type, _, :map, :any}) when is_map(value)
         def match_type(value, {:type, _, :pid, []}) when is_pid(value)
         def match_type(value, {:type, _, :pid, []} = type)
         def match_type(value, {:type, _, :port, []}) when is_port(value)
         def match_type(value, {:type, _, :port, []} = type)

It would be very cool if Hammox could verify it.

Should stubs be `protected` too?

As I was reading through the code, I noticed that while expect wraps the replacement implementation with protected, stub (and stub_with) do not.

Is there a reason for this decision? Would you consider a PR that wraps stub's code with protected?

I'm thinking of an implementation like this:

def stub(mock, name, code) do
    arity = :erlang.fun_info(code)[:arity]

    hammox_code =
      case fetch_typespecs_for_mock(mock, name, arity) do
        # This is really an error case where we're trying to mock a function
        # that does not exist in the behaviour. Mox will flag it better though
        # so just let it pass through.
        [] -> code
        typespecs -> protected(code, typespecs, arity)
      end

    Mox.stub(mock, name, hammox_code)
  end

I'm not sure what a protected implementation for stub_with would look like, though, so ideas are welcome there.

Thanks for this awesome library!

How to test bad behaviour

Suppose I have a GenServer that calls a callback module implementing some behaviour.

I want to test that my GenServer does the right thing when the module does not return expected values. But hammox will not let me badly implement a behaviour.

Is there a way to opt-out type checking for a mock ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.