{"id":13491665,"url":"https://github.com/msz/hammox","last_synced_at":"2025-04-10T06:18:32.800Z","repository":{"id":35136245,"uuid":"194456033","full_name":"msz/hammox","owner":"msz","description":"🏝 automated contract testing via type checking for Elixir functions and mocks","archived":false,"fork":false,"pushed_at":"2023-11-30T04:40:18.000Z","size":341,"stargazers_count":566,"open_issues_count":19,"forks_count":28,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-12-06T19:38:37.555Z","etag":null,"topics":["behaviour","behaviour-typespec","behaviours","contract","contract-testing","contracts","dialyzer","elixir","explicit-contracts","mock","mocks","mox","testing","type-checker","type-checking","typechecker","typechecking","typespec","typespecs","unit-testing"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/msz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-06-29T23:15:53.000Z","updated_at":"2024-11-27T16:51:18.000Z","dependencies_parsed_at":"2024-01-16T09:44:27.069Z","dependency_job_id":"111b7a2b-6f71-49e1-aa9f-5e4daa2059de","html_url":"https://github.com/msz/hammox","commit_stats":{"total_commits":231,"total_committers":20,"mean_commits":11.55,"dds":0.6233766233766234,"last_synced_commit":"8f070dda7daa9375abea776834a083ee16461146"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msz%2Fhammox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msz%2Fhammox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msz%2Fhammox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msz%2Fhammox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/msz","download_url":"https://codeload.github.com/msz/hammox/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248166863,"owners_count":21058481,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["behaviour","behaviour-typespec","behaviours","contract","contract-testing","contracts","dialyzer","elixir","explicit-contracts","mock","mocks","mox","testing","type-checker","type-checking","typechecker","typechecking","typespec","typespecs","unit-testing"],"created_at":"2024-07-31T19:00:59.172Z","updated_at":"2025-04-10T06:18:32.773Z","avatar_url":"https://github.com/msz.png","language":"Elixir","readme":"# Hammox\n\n[![CI](https://github.com/msz/hammox/actions/workflows/ci.yml/badge.svg)](https://github.com/msz/hammox/actions/workflows/ci.yml)\n[![Module Version](https://img.shields.io/hexpm/v/hammox.svg)](https://hex.pm/packages/hammox)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/hammox/)\n[![Total Download](https://img.shields.io/hexpm/dt/hammox.svg)](https://hex.pm/packages/hammox)\n[![License](https://img.shields.io/hexpm/l/hammox.svg)](https://github.com/msz/hammox/blob/master/LICENSE)\n[![Last Updated](https://img.shields.io/github/last-commit/msz/hammox.svg)](https://github.com/msz/hammox/commits/master)\n\nHammox is a library for rigorous unit testing using mocks, explicit\nbehaviours and contract tests. You can use it to ensure both your mocks and\nimplementations fulfill the same contract.\n\nIt takes the excellent [Mox](https://github.com/plataformatec/mox) library\nand pushes its philosophy to its limits, providing automatic contract tests\nbased on behaviour typespecs while maintaining full compatibility with code\nalready using Mox.\n\nHammox aims to catch as many contract bugs as possible while providing useful\ndeep stacktraces so they can be easily tracked down and fixed.\n\n## Installation\n\nIf you are currently using [Mox](https://github.com/plataformatec/mox),\ndelete it from your list of dependencies in `mix.exs`. Then add `:hammox`:\n\n```elixir\ndef deps do\n  [\n    {:hammox, \"~\u003e 0.7\", only: :test}\n  ]\nend\n```\n\n## Starting from scratch\n\nRead [\"Mocks and explicit contracts\"](http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/)\nby José Valim. Then proceed to the [Mox documentation](https://hexdocs.pm/mox/Mox.html).\nOnce you are comfortable with Mox, switch to using Hammox.\n\n## Migrating from Mox\n\nReplace all occurrences of `Mox` with `Hammox`. Nothing more is required; all\nyour mock calls in test are now ensured to conform to the behaviour typespec.\n\n## Example\n\n### Typical mock setup\n\nLet's say we have a database which can get us user data. We have a module,\n`RealDatabase` (not shown), which implements the following behaviour:\n```elixir\ndefmodule Database do\n  @callback get_users() :: [binary()]\nend\n```\nWe use this client in a `Stats` module which can aggregate data about users:\n```elixir\ndefmodule Stats do\n  def count_users(database \\\\ RealDatabase) do\n    length(database.get_users())\n  end\nend\n```\nAnd we create a unit test for it:\n```elixir\ndefmodule StatsTest do\n  use ExUnit.Case, async: true\n\n  test \"count_users/0 returns correct user count\" do\n    assert 2 == Stats.count_users()\n  end\nend\n```\n\nFor this test to work, we would have to start a real instance of the database\nand provision it with two users. This is of course unnecessary brittleness —\nin a unit test, we only want to test that our Stats code provides correct\nresults given specific inputs. To simplify, we will create a mocked Database\nusing Mox and use it in the test:\n\n```elixir\ndefmodule StatsTest do\n  use ExUnit.Case, async: true\n  import Mox\n\n  test \"count_users/0 returns correct user count\" do\n    defmock(DatabaseMock, for: Database)\n    expect(DatabaseMock, :get_users, fn -\u003e\n      [\"joe\", \"jim\"]\n    end)\n\n    assert 2 == Stats.count_users(DatabaseMock)\n  end\nend\n```\nThe test now passes as expected.\n\n### The contract breaks\n\nImagine that some time later we want to add error flagging for our database\nclient. We change `RealDatabase` and the corresponding behaviour, `Database`,\nto return an ok/error tuple instead of a raw value:\n```elixir\ndefmodule Database do\n  @callback get_users() :: {:ok, [binary()]} | {:error, term()}\nend\n```\n\nHowever, The `Stats.count_users/0` test *will still pass*, even though the\nfunction will break when the real database client is used! This is because\nthe mock is now invalid — it no longer implements the given behaviour, and\ntherefore breaks the contract. Even though Mox is supposed to create mocks\nfollowing explicit contracts, it does not take typespecs into account.\n\nThis is where Hammox comes in. Simply replace all occurrences of Mox with\nHammox (for example, `import Mox` becomes `import Hammox`, etc) and you\nwill now get this when trying to run the test:\n\n```none\n** (Hammox.TypeMatchError)\nReturned value [\"joe\", \"jim\"] does not match type {:ok, [binary()]} | {:error, term()}.\n```\n\nNow the consistency between the mock and its behaviour is enforced.\n\n### Completing the triangle\n\nHammox automatically checks mocks with behaviours, but what about the real\nimplementations? The real goal is to keep all units implementing a given\nbehaviour in sync.\n\nYou can decorate any function with Hammox checks by using `Hammox.protect/2`.\nIt will return an anonymous function which you can use in place of the\noriginal module function. An example test:\n\n```elixir\ndefmodule RealDatabaseTest do\n  use ExUnit.Case, async: true\n\n  test \"get_users/0 returns list of users\" do\n    get_users_0 = Hammox.protect({RealDatabase, :get_users, 0}, Database)\n    assert {:ok, [\"real-jim\", \"real-joe\"]} == get_users_0.()\n  end\nend\n```\n\nIt's a good idea to put setup logic like this in a `setup_all` hook and then\naccess the protected functions using the test context:\n\n```elixir\ndefmodule RealDatabaseTest do\n  use ExUnit.Case, async: true\n\n  setup_all do\n    %{get_users_0: Hammox.protect({RealDatabase, :get_users, 0}, Database)}\n  end\n\n  test \"get_users/0 returns list of users\", %{get_users_0: get_users_0} do\n    assert {:ok, [\"real-jim\", \"real-joe\"]} == get_users_0.()\n  end\nend\n```\n\nHammox also provides a `setup_all` friendly version of `Hammox.protect` which\nleverages this pattern. Simply pass both the implementation module and the\nbehaviour module and you will get a map of all callbacks defined by the\nbehaviour as decorated implementation functions.\n\n```elixir\ndefmodule RealDatabaseTest do\n  use ExUnit.Case, async: true\n\n  setup_all do\n    Hammox.protect(RealDatabase, Database)\n  end\n\n  test \"get_users/0 returns list of users\", %{get_users_0: get_users_0} do\n    assert {:ok, [\"real-jim\", \"real-joe\"]} == get_users_0.()\n  end\nend\n```\n\nAlternatively, if you're up for trading explicitness for some macro magic,\nyou can use `use Hammox.Protect` to locally define protected versions of\nfunctions you're testing, as if you `import`ed the module:\n\n```elixir\ndefmodule RealDatabaseTest do\n  use ExUnit.Case, async: true\n  use Hammox.Protect, module: RealDatabase, behaviour: Database\n\n  test \"get_users/0 returns list of users\" do\n    assert {:ok, [\"real-jim\", \"real-joe\"]} == get_users()\n  end\nend\n```\n\n## Why use Hammox for my application code when I have Dialyzer?\n\nDialyzer cannot detect Mox style mocks not conforming to typespec.\n\nThe main aim of Hammox is to enforce consistency between behaviours, mocks\nand implementations. This is best achieved when both mocks and\nimplementations are subjected to the exact same checks.\n\nDialyzer is a static analysis tool; Hammox is a dynamic contract test\nprovider. They operate differently and one can catch some bugs when the other\ndoesn't. While it is true that Hammox would be redundant given a strong,\nstrict, TypeScript-like type system for Elixir, Dialyzer is far from providing\nthat sort of coverage.\n\n## Protocol types\n\nA `t()` type defined on a protocol is taken by Hammox to mean \"a struct\nimplementing the given protocol\". Therefore, trying to pass `:atom` for an\n`Enumerable.t()` will produce an error, even though the type is defined as\n`term()`:\n\n```none\n** (Hammox.TypeMatchError)\nReturned value :atom does not match type Enumerable.t().\n  Value :atom does not implement the Enumerable protocol.\n```\n\n## Disable protection for specific mocks\n\nHammox also includes Mox as a dependency. This means that if you would like\nto disable Hammox protection for a specific mock, you can simply use vanilla\nMox for that specific instance. They will interoperate without problems.\n\n## Limitations\n- For anonymous function types in typespecs, only the arity is checked.\nParameter types and return types are not checked.\n\n## Telemetry\n\nHammox now includes telemetry events! See [Telemetry Guide](https://hexdocs.pm/hammox/Telemetry.html) for more information.\n\n## License\n\nCopyright 2019 Michał Szewczak\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law\nor agreed to in writing, software distributed under the License is\ndistributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the specific language\ngoverning permissions and limitations under the License.\n","funding_links":[],"categories":["Elixir","testing"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsz%2Fhammox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmsz%2Fhammox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsz%2Fhammox/lists"}