Skip to content

Instantly share code, notes, and snippets.

@churcho
Forked from zkayser/unverified_mocks.ex
Created April 17, 2024 15:46
Show Gist options
  • Save churcho/2b2d4513a88874f0e12394ee95287b52 to your computer and use it in GitHub Desktop.
Save churcho/2b2d4513a88874f0e12394ee95287b52 to your computer and use it in GitHub Desktop.
Credo Check for Validating Mox Users Verify Expectations
defmodule MyApp.Checks.UnverifiedMocks do
@moduledoc """
#{__MODULE__} looks for test files that import Mox and use
the `expect/4` function, but do not enforce any assertions that
the expectations have been called or not either by running `verify_on_exit`
from a setup block or calling `verify!/0` or `verify!/1` inline
in a test block.
"""
@message """
Credo found a test file that imports Mox and uses expect/4, but no verifications were found. When you use expect/4, make sure that you are verifying the the mock.
To do this, either make sure that you also add:
setup :verify_on_exit!
to your test file, or alternatively, call verify!/0 or verify!/1 in each test that uses expect/4
"""
@exit_status 32
# Set up the behaviour and make this module a "check":
use Credo.Check,
base_priority: :high,
category: :warning,
param_defaults: [],
explanations: [
check: """
:verify_on_exit! for tests that need Mock
""",
params: []
],
exit_status: @exit_status
@doc """
Inspects test files for the use of `Mox.expect/4` where the expectations being made are unverified. If unverified expectations are found, this check will flag the test file as an issue.
The offending file will be displayed in the resulting issue when running `mix credo --strict`, and the line number indicated in the issue will point to the first use of `expect` in that file where the expectation has not been verified.
"""
@impl Credo.Check
def run(source_file, params \\ []) do
issue_meta = IssueMeta.for(source_file, params)
walked_directives =
source_file
|> Credo.Code.ast()
|> then(fn {:ok, ast} -> Macro.postwalker(ast) end)
with true <-
Enum.any?(walked_directives, fn ast_node ->
match?({:import, _, [{_, _, [:Mox]}]}, ast_node)
end),
{:expect, context, _} <-
find_unverified_expect(walked_directives),
false <-
Enum.any?(walked_directives, &setup_contains_verify_on_exit?/1) do
[issue_for("Missing verify_on_exit!", context, issue_meta)]
else
_ -> []
end
end
defp find_unverified_expect(walked_directives) do
Enum.find_value(walked_directives, fn ast_node ->
with test_block when not is_nil(test_block) <- find_test_body(ast_node),
{:expect, _context, _} = expect_tuple <-
Enum.find(test_block, &match?({:expect, _, _}, &1)),
false <- Enum.any?(test_block, &match?({:verify!, _, _}, &1)) do
expect_tuple
else
_ -> false
end
end)
end
defp find_test_body(ast_node) do
case ast_node do
# multiline test without context
{:test, _, [_, [do: {:__block__, _context, test_body}]]} ->
test_body
# multiline test with context
{:test, _, [_, _, [do: {:__block__, _context, test_body}]]} ->
test_body
# singleline test without context
{:test, _, [_, [do: {:expect, _, _} = test_body]]} ->
[test_body]
# singleline test with context
{:test, _, [_, _, [do: {:expect, _, _} = test_body]]} ->
[test_body]
_ ->
nil
end
end
defp setup_contains_verify_on_exit?({:setup, _context, [:verify_on_exit!]}), do: true
defp setup_contains_verify_on_exit?({:setup, _context, [[do: block_ast]]}) do
Enum.any?(Macro.prewalker(block_ast), fn node ->
match?({:verify_on_exit!, _, _}, node)
end)
end
defp setup_contains_verify_on_exit?({:setup, _context, [_, [do: block_ast]]}) do
Enum.any?(Macro.postwalker(block_ast), fn node ->
match?({:verify_on_exit!, _, _}, node)
end)
end
defp setup_contains_verify_on_exit?({:setup, _context, [arguments]}) when is_list(arguments) do
Enum.member?(arguments, :verify_on_exit!)
end
defp setup_contains_verify_on_exit?(_), do: false
defp issue_for(name, context, issues_meta) do
format_issue(
issues_meta,
message: @message,
trigger: name,
line_no: context[:line]
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment