case.ex (5756B)
1 defmodule Credo.Test.Case do 2 @moduledoc """ 3 Conveniences for testing Credo custom checks and plugins. 4 5 This module can be used in your test cases, like this: 6 7 use Credo.Test.Case 8 9 Using this module will: 10 11 * import all the functions from this module 12 * make the test case `:async` by default (use `use Credo.Test.Case, async: false` to opt out) 13 14 ## Testing custom checks 15 16 Suppose we have a custom check in our project that checks whether or not 17 the "FooBar rules" are applied (one of those *very* project-specific things). 18 19 defmodule MyProject.MyCustomChecks.FooBar do 20 use Credo.Check, category: :warning, base_priority: :high 21 22 def run(%SourceFile{} = source_file, params) do 23 # ... implement all the "FooBar rules" ... 24 end 25 end 26 27 When we want to test this check, we can use `Credo.Test.Case` for convenience: 28 29 defmodule MyProject.MyCustomChecks.FooBarTest do 30 use Credo.Test.Case 31 32 alias MyProject.MyCustomChecks.FooBar 33 34 test "it should NOT report expected code" do 35 \"\"\" 36 defmodule CredoSampleModule do 37 # ... some good Elixir code ... 38 end 39 \"\"\" 40 |> to_source_file() 41 |> run_check(FooBar) 42 |> refute_issues() 43 end 44 45 test "it should report code that violates the FooBar rule" do 46 \"\"\" 47 defmodule CredoSampleModule do 48 # ... some Elixir code that violates the FooBar rule ... 49 end 50 \"\"\" 51 |> to_source_file() 52 |> run_check(FooBar) 53 |> assert_issues() 54 end 55 end 56 57 This is as simple and mundane as it looks (which is a good thing): 58 We have two tests: one for the good case, one for the bad case. 59 In each, we create a source file representation from a heredoc, run our custom check and assert/refute the issues 60 we expect. 61 62 ## Asserting found issues 63 64 Once we get to know domain a little better, we can add more tests, typically testing for other bad cases in which 65 our check should produce issues. 66 67 Note that there are two assertion functions for this: `assert_issue/2` and `assert_issues/2`, where the first one 68 ensures that there is a single issue and the second asserts that there are at least two issues. 69 70 Both functions take an optional `callback` as their second parameter, which is called with the `issue` or the 71 list of `issues` found, which makes it convenient to check for the issues properties ... 72 73 \"\"\" 74 # ... any Elixir code ... 75 \"\"\" 76 |> to_source_file() 77 |> run_check(FooBar) 78 |> assert_issue(fn issue -> assert issue.trigger == "foo" end) 79 80 ... or properties of the list of issues: 81 82 \"\"\" 83 # ... any Elixir code ... 84 \"\"\" 85 |> to_source_file() 86 |> run_check(FooBar) 87 |> assert_issue(fn issues -> assert Enum.count(issues) == 3 end) 88 89 ## Testing checks that analyse multiple source files 90 91 For checks that analyse multiple source files, like Credo's consistency checks, we can use `to_source_files/1` to 92 create 93 94 [ 95 \"\"\" 96 # source file 1 97 \"\"\", 98 \"\"\" 99 # source file 2 100 \"\"\" 101 ] 102 |> to_source_files() 103 |> run_check(FooBar) 104 |> refute_issues() 105 106 If our check needs named source files, we can always use `to_source_file/2` to create individually named source 107 files and combine them into a list: 108 109 source_file1 = 110 \"\"\" 111 # source file 1 112 \"\"\" 113 |> to_source_file("foo.ex") 114 115 source_file2 = 116 \"\"\" 117 # source file 2 118 \"\"\" 119 |> to_source_file("bar.ex") 120 121 [source_file1, source_file2] 122 |> run_check(FooBar) 123 |> assert_issue(fn issue -> assert issue.filename == "foo.ex" end) 124 """ 125 defmacro __using__(opts) do 126 async = opts[:async] != false 127 128 quote do 129 use ExUnit.Case, async: unquote(async) 130 131 import Credo.Test.Case 132 end 133 end 134 135 alias Credo.Test.Assertions 136 alias Credo.Test.CheckRunner 137 alias Credo.Test.SourceFiles 138 139 @doc """ 140 Refutes the presence of any issues. 141 """ 142 def refute_issues(issues) do 143 Assertions.refute_issues(issues) 144 end 145 146 @doc """ 147 Asserts the presence of a single issue. 148 """ 149 def assert_issue(issues, callback \\ nil) do 150 Assertions.assert_issue(issues, callback) 151 end 152 153 @doc """ 154 Asserts the presence of more than one issue. 155 """ 156 def assert_issues(issues, callback \\ nil) do 157 Assertions.assert_issues(issues, callback) 158 end 159 160 @doc false 161 # TODO: remove this 162 def assert_trigger(issue, trigger) do 163 Assertions.assert_trigger(issue, trigger) 164 end 165 166 # 167 168 @doc """ 169 Runs the given `check` on the given `source_file` using the given `params`. 170 171 "x = 5" 172 |> to_source_file() 173 |> run_check(MyProject.MyCheck, foo_parameter: "bar") 174 """ 175 def run_check(source_file, check, params \\ []) do 176 CheckRunner.run_check(source_file, check, params) 177 end 178 179 # 180 181 @doc """ 182 Converts the given `source` string to a `%SourceFile{}`. 183 184 "x = 5" 185 |> to_source_file() 186 """ 187 def to_source_file(source) when is_binary(source) do 188 SourceFiles.to_source_file(source) 189 end 190 191 @doc """ 192 Converts the given `source` string to a `%SourceFile{}` with the given `filename`. 193 194 "x = 5" 195 |> to_source_file("simple.ex") 196 """ 197 def to_source_file(source, filename) when is_binary(source) and is_binary(filename) do 198 SourceFiles.to_source_file(source, filename) 199 end 200 201 @doc """ 202 Converts the given `list` of source code strings to a list of `%SourceFile{}` structs. 203 204 ["x = 5", "y = 6"] 205 |> to_source_files() 206 """ 207 def to_source_files(list) when is_list(list) do 208 Enum.map(list, &to_source_file/1) 209 end 210 end