Created
April 17, 2024 13:06
-
-
Save zkayser/66539f2891389030b764b8841452beee to your computer and use it in GitHub Desktop.
Credo Check for Validating Mox Users Verify Expectations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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