Module that helps you test your rules, using elm-test.
import Review.Test
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
tests : Test
tests =
describe "The.Rule.You.Want.To.Test"
[ test "should not report anything when <condition>" <|
\() ->
"""module A exposing (..)
a = foo n"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should report Debug.log use" <|
\() ->
"""module A exposing (..)
a = Debug.log "some" "message" """
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "Details about the error" ]
, under = "Debug.log"
}
]
]Writing a rule is a process that works really well with the Test-Driven Development process loop, which is:
Then repeat for every pattern yoy wish to handle.
A good test title explains
describe
might improve your test report. Or maybe you are testing a sub-part of the rule,
and you can name it explictly.Ideally, by only reading through the test titles, someone else should be able to rewrite the rule you are testing.
You should test the scenarios where you expect the rule to report something. At the same time, you should also test when it shouldn't. I encourage writing tests to make sure that things that are similar to what you want to report are not reported.
For instance, if you wish to report uses of variables named foo, write a test
that ensures that the use of variables named differently does not get reported.
Tests are pretty cheap, and in the case of rules, it is probably better to have too many tests rather than too few, since the behavior of a rule rarely changes drastically.
If you are interested, you can read the design goals for this module.
Run a Rule on a String containing source code. You can then use
expectNoErrors or expectErrors to assert
the errors reported by the rule.
import My.Rule exposing (rule)
import Review.Test
import Test exposing (Test, test)
all : Test
all =
test "test title" <|
\() ->
"""
module SomeModule exposing (a)
a = 1"""
|> Review.Test.run rule
|> Review.Test.expectNoErrorsThe source code needs to be syntactically valid Elm code. If the code can't be parsed, the test will fail regardless of the expectations you set on it.
Note that to be syntactically valid, you need at least a module declaration at the
top of the file (like module A exposing (..)) and one declaration (like a = 1).
You can't just have an expression like 1 + 2.
Note: This is a simpler version of runWithProjectData.
If your rule is interested in project related details, then you should use
runWithProjectData instead.
Run a Rule on a String containing source code, with data about the
project loaded, such as the contents of elm.json file.
import My.Rule exposing (rule)
import Review.Project as Project exposing (Project)
import Review.Test
import Test exposing (Test, test)
all : Test
all =
test "test title" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson elmJsonToConstructManually
in
"""module SomeModule exposing (a)
a = 1"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectNoErrorsThe source code needs to be syntactically valid Elm code. If the code can't be parsed, the test will fail regardless of the expectations you set on it.
Note that to be syntactically valid, you need at least a module declaration at the
top of the file (like module A exposing (..)) and one declaration (like a = 1).
You can't just have an expression like 1 + 2.
Note: This is a more complex version of run. If your rule is not
interested in project related details, then you should use run instead.
Run a Rule on several modules. You can then use
expectNoErrors or expectErrorsForModules to assert
the errors reported by the rule.
This is the same as run, but you can pass several modules.
This is especially useful to test rules created with
Review.Rule.newProjectRuleSchema, that look at
several files, and where the context of the project is important.
import My.Rule exposing (rule)
import Review.Test
import Test exposing (Test, test)
all : Test
all =
test "test title" <|
\() ->
[ """
module A exposing (a)
a = 1""", """
module B exposing (a)
a = 1""" ]
|> Review.Test.runOnModules rule
|> Review.Test.expectNoErrorsThe source codes need to be syntactically valid Elm code. If the code can't be parsed, the test will fail regardless of the expectations you set on it.
Note that to be syntactically valid, you need at least a module declaration at the
top of each file (like module A exposing (..)) and one declaration (like a = 1).
You can't just have an expression like 1 + 2.
Note: This is a simpler version of runOnModulesWithProjectData.
If your rule is interested in project related details, then you should use
runOnModulesWithProjectData instead.
Run a Rule on several modules. You can then use
expectNoErrors or expectErrorsForModules to assert
the errors reported by the rule.
This is basically the same as run, but you can pass several modules.
This is especially useful to test rules created with
Review.Rule.newProjectRuleSchema, that look at
several modules, and where the context of the project is important.
import My.Rule exposing (rule)
import Review.Test
import Test exposing (Test, test)
all : Test
all =
test "test title" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson elmJsonToConstructManually
in
[ """
module A exposing (a)
a = 1""", """
module B exposing (a)
a = 1""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrorsThe source codes need to be syntactically valid Elm code. If the code can't be parsed, the test will fail regardless of the expectations you set on it.
Note that to be syntactically valid, you need at least a module declaration at the
top of each file (like module A exposing (..)) and one declaration (like a = 1).
You can't just have an expression like 1 + 2.
Note: This is a more complex version of runOnModules. If your rule is not
interested in project related details, then you should use runOnModules instead.
An expectation for an error. Use error to create one.
Assert that the rule reported no errors. Note, this is equivalent to using expectErrors
like expectErrors [].
import Review.Test
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
tests : Test
tests =
describe "The.Rule.You.Want.To.Test"
[ test "should not report anything when <condition>" <|
\() ->
"""module A exposing (..)
a = foo n"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
]Assert that the rule reported some errors, by specifying which ones.
Assert which errors are reported using error. The test will fail if
a different number of errors than expected are reported, or if the message or the
location is incorrect.
The errors should be in the order of where they appear in the source code. An error at the start of the source code should appear earlier in the list than an error at the end of the source code.
import Review.Test
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
tests : Test
tests =
describe "The.Rule.You.Want.To.Test"
[ test "should report Debug.log use" <|
\() ->
"""module A exposing (..)
a = Debug.log "some" "message"
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "Details about the error" ]
, under = "Debug.log"
}
]
]Create an expectation for an error.
message should be the message you're expecting to be shown to the user.
under is the part of the code where you are expecting the error to be shown to
the user. If it helps, imagine under to be the text under which the squiggly
lines will appear if the error appeared in an editor.
tests : Test
tests =
describe "The.Rule.You.Want.To.Test"
[ test "should report Debug.log use" <|
\() ->
"""module A exposing (..)
a = Debug.log "some" "message\""""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "Details about the error" ]
, under = "Debug.log"
}
]
]If there are multiple locations where the value of under appears, the test will
fail unless you use atExactly to remove any ambiguity of where the
error should be used.
Precise the exact position where the error should be shown to the user. This
is only necessary when the under field is ambiguous.
atExactly takes a record with start and end positions.
tests : Test
tests =
describe "The.Rule.You.Want.To.Test"
[ test "should report multiple Debug.log calls" <|
\() ->
"""module A exposing (..)
a = Debug.log "foo" z
b = Debug.log "foo" z
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "Details about the error" ]
, under = "Debug.log"
}
|> Review.Test.atExactly { start = { row = 4, column = 5 }, end = { row = 4, column = 14 } }
, Review.Test.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "Details about the error" ]
, under = "Debug.log"
}
|> Review.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 14 } }
]
]Tip: By default, do not use this function. If the test fails because there is some
ambiguity, the test error will give you a recommendation of what to use as a parameter
of atExactly, so you do not have to bother writing this hard-to-write argument yourself.
Create an expectation that the error provides an automatic fix, meaning that it used
functions like errorWithFix, and an expectation of what the source
code should be after the error's fix have been applied.
In the absence of whenFixed, the test will fail if the error provides a fix.
In other words, you only need to use this function if the error provides a fix.
tests : Test
tests =
describe "The.Rule.You.Want.To.Test"
[ test "should report multiple Debug.log calls" <|
\() ->
"""module A exposing (..)
a = 1
b = Debug.log "foo" 2
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "Details about the error" ]
, under = "Debug.log"
}
|> Review.Test.whenFixed """module SomeModule exposing (b)
a = 1
b = 2
"""
]
]Assert that the rule reported some errors, by specifying which ones and the module for which they were reported.
This is the same as expectErrors, but for when you used
runOnModules or runOnModulesWithProjectData.
to create the test. When using those, the errors you expect need to be associated
with a module. If we don't specify this, your tests might pass because you
expected the right errors, but they may be reported for the wrong module!
The expected errors are tupled: the first element is the module name
(for example: List or My.Module.Name) and the second element is the list of
errors you expect to be reported.
Assert which errors are reported using error. The test will fail if
a different number of errors than expected are reported, or if the message or the
location is incorrect.
The errors should be in the order of where they appear in the source code. An error at the start of the source code should appear earlier in the list than an error at the end of the source code.
import Review.Test
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
all : Test
all =
test "should report an error when a module uses `Debug.log`" <|
\() ->
[ """
module ModuleA exposing (a)
a = 1""", """
module ModuleB exposing (a)
a = Debug.log "log" 1""" ]
|> Review.Test.runOnModules rule
|> Review.Test.expectErrorsForModules
[ ( "ModuleB"
, [ Review.Test.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "Details about the error" ]
, under = "Debug.log"
}
]
)
]Assert that the rule reported some errors for the elm.json file, by specifying which ones.
test "report an error when a module is unused" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson elmJsonToConstructManually
in
"""
module ModuleA exposing (a)
a = 1"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectErrorsForElmJson
[ Review.Test.error
{ message = "Unused dependency `author/package`"
, details = [ "Dependency should be removed" ]
, under = "author/package"
}
]Alternatively, or if you need to specify errors for other files too, you can use expectErrorsForModules, specifying elm.json as the module name.
sourceCode
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "ModuleB", [ Review.Test.error someErrorModuleB ] )
, ( "elm.json", [ Review.Test.error someErrorForElmJson ] )
]Assert which errors are reported using error. The test will fail if
a different number of errors than expected are reported, or if the message or the
location is incorrect.
Assert that the rule reported some errors for the README.md file, by specifying which ones.
test "report an error when a module is unused" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addReadme { path = "README.md", context = "# Project\n..." }
in
"""
module ModuleA exposing (a)
a = 1"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectErrorsForReadme
[ Review.Test.error
{ message = "Invalid link"
, details = [ "README contains an invalid link" ]
, under = "htt://example.com"
}
]Alternatively, or if you need to specify errors for other files too, you can use expectErrorsForModules, specifying README.md as the module name.
sourceCode
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "ModuleB", [ Review.Test.error someErrorModuleB ] )
, ( "README.md", [ Review.Test.error someErrorForReadme ] )
]Assert which errors are reported using error. The test will fail if
a different number of errors than expected are reported, or if the message or the
location is incorrect.
The result of running a rule on a
Stringcontaining source code.