Troubleshooting failures
You can add diagnostic information to your tests by using clues and log statements.
You should add information when you write a test, and not just when it fails. This will save you needing to write more code when your test fails later.
This information will only be displayed when a test fails, so will not clutter your test report.
Using clue with expect
The expect and expect.all statements are used to assert on boolean values. On their own, they don't print any diagnostic information.
You can add information by using the clue function.
val x = List(1, 2, 3)
expect(clue(x.size) > 2)
You can add clues around any variable.
expect(clue(x).size > 2)
You can also nest them.
expect(clue(clue(x).size) > 2)
The clues are displayed when the test fails.
import weaver._
object ExpectWithClueTest extends FunSuite {
test("This fails, and we can diagnose why") {
val x = 1
val y = 2
expect(clue(x) > clue(y))
}
test("This fails, but we don't know why") {
val x = 1
val y = 2
expect(x > y)
}
test("This succeeds so nothing is printed") {
val x = 1
val y = 0
expect(clue(x) > clue(y))
}
}
Example report
repl.MdocSessionMdocAppExpectWithClueTest + This succeeds so nothing is printed 0ms - This fails, and we can diagnose why 0ms - This fails, but we don't know why 0ms *************FAILURES************* repl.MdocSessionMdocAppExpectWithClueTest - This fails, and we can diagnose why 0ms
assertion failed (troubleshooting_failures.md:46)
expect(clue(x) > clue(y))
Clues {
x: Int = 1
y: Int = 2
}
troubleshooting_failures.md:46
expect(clue(x) > clue(y))
^ - This fails, but we don't know why 0ms
assertion failed (troubleshooting_failures.md:52)
expect(x > y)
Use the `clue` function to troubleshoot
troubleshooting_failures.md:52
expect(x > y)
^ Total 3, Failed 2, Passed 1, Ignored 0
Logging information
You can log general information using a lazy logger. Log statements are only printed when a test fails and are displayed along with the test failure.
Access the logger by writing a loggedTest.
import weaver._
object LoggedTests extends SimpleIOSuite {
case class Response(body: String, status: Int)
loggedTest("This test fails") { log =>
val response = Response("this went wrong", 500)
for {
_ <- log.info(s"The response body is: ${response.body}")
} yield expect.eql(200, response.status)
}
loggedTest("This test also fails, and has separate logs") { log =>
val response = Response("this also went wrong", 400)
for {
_ <- log.info(s"The response body is: ${response.body}")
} yield expect.eql(200, response.status)
}
loggedTest("This test succeeds, so no logs are printed") { log =>
val response = Response("OK", 200)
for {
_ <- log.info(s"The response body is: ${response.body}")
} yield expect.eql(200, response.status)
}
}
Example report
repl.MdocSessionMdocAppLoggedTests + This test succeeds, so no logs are printed 8ms - This test fails 9ms - This test also fails, and has separate logs 8ms *************FAILURES************* repl.MdocSessionMdocAppLoggedTests - This test fails 9ms
Values not equal: (troubleshooting_failures.md:83)
in expect.eql(- expected, + found)
-200
+500
troubleshooting_failures.md:83
} yield expect.eql(200, response.status)
^
[INFO] 11:40:40 [troubleshooting_failures.md:82] The response body is: this went wrong - This test also fails, and has separate logs 8ms
Values not equal: (troubleshooting_failures.md:90)
in expect.eql(- expected, + found)
-200
+400
troubleshooting_failures.md:90
} yield expect.eql(200, response.status)
^
[INFO] 11:40:40 [troubleshooting_failures.md:89] The response body is: this also went wrong Total 3, Failed 2, Passed 1, Ignored 0
Use log.info instead of println
You should use log statements instead of println statements.
Because weaver runs tests in parallel, it is difficult to pinpoint which test a println statement corresponds to.
Unlike log statements, println statements are always printed, regardless of whether a test succeeds or fails, so can clutter the test output.
Log statements are only printed if a test fails. The logs statements are displayed along with the test failure they correspond to, so it's easy to troubleshoot tests even though they are run in parallel.
Accessing the logger along with shared resources
You can access the logger along with shared resources by using the second argument of the test function.
import weaver._
import cats.effect._
import org.http4s.ember.client._
import org.http4s.client._
object HttpSuite extends IOSuite {
override type Res = Client[IO]
override def sharedResource : Resource[IO, Res] =
EmberClientBuilder.default[IO].build
// The log is passed as the second argument
test("This test fails") { (httpClient, log) =>
for {
statusCode <- httpClient.get("https://httpbin.org/oops") {
response => for {
_ <- log.info(s"Content length: ${response.contentLength}")
} yield response.status.code
}
} yield expect.eql(200, statusCode)
}
}
Example report
repl.MdocSessionMdocAppHttpSuite - This test fails 12s *************FAILURES************* repl.MdocSessionMdocAppHttpSuite - This test fails 12s
Values not equal: (troubleshooting_failures.md:137)
in expect.eql(- expected, + found)
-200
+404
troubleshooting_failures.md:137
} yield expect.eql(200, statusCode)
^
[INFO] 11:40:53 [troubleshooting_failures.md:134] Content length: Some(233) Total 1, Failed 1, Passed 0, Ignored 0
Displaying the correct source location
Assertions such as expect and expect.eql display their source location when they fail.
If you make assertions in helper functions, you might want to display the source location of the call to the helper.
You can do this by requiring an implicit SourceLocation argument.
import weaver._
object SourceLocationSuite extends FunSuite {
case class Response(body: String, status: Int)
test("This test fails") {
val response = Response("this went wrong", 500)
expectOk(response) // The failure points here
}
test("This test fails with a different source location") {
val response = Response("this also went wrong", 500)
expectOk(response) // The other failure points here
}
def expectOk(response: Response)(implicit loc: SourceLocation): Expectations = {
expect.eql(200, response.status) && expect.eql("OK", response.body)
}
}
Example report
repl.MdocSessionMdocAppSourceLocationSuite - This test fails 2ms - This test fails with a different source location 0ms *************FAILURES************* repl.MdocSessionMdocAppSourceLocationSuite - This test fails 2ms
[0] Values not equal: (troubleshooting_failures.md:160)
[0]
[0] in expect.eql(- expected, + found)
[0] -200
[0] +500
[0]
[0] troubleshooting_failures.md:160
[0] expectOk(response) // The failure points here
[0] ^
[1] Values not equal: (troubleshooting_failures.md:160)
[1]
[1] in expect.eql(- expected, + found)
[1] -OK
[1] +this went wrong
[1]
[1] troubleshooting_failures.md:160
[1] expectOk(response) // The failure points here
[1] ^ - This test fails with a different source location 0ms
[0] Values not equal: (troubleshooting_failures.md:165)
[0]
[0] in expect.eql(- expected, + found)
[0] -200
[0] +500
[0]
[0] troubleshooting_failures.md:165
[0] expectOk(response) // The other failure points here
[0] ^
[1] Values not equal: (troubleshooting_failures.md:165)
[1]
[1] in expect.eql(- expected, + found)
[1] -OK
[1] +this also went wrong
[1]
[1] troubleshooting_failures.md:165
[1] expectOk(response) // The other failure points here
[1] ^ Total 2, Failed 2, Passed 0, Ignored 0
Displaying a trace
If you have a test codebase with many nested helper functions, you may want to display a trace of source locations. You can do this with the traced(here) function.
In the following example, expectOk calls expectStatus and expectOkBody. When the test fails, the source location of both expectStatus and expectOk is displayed.
import weaver._
object TracingSuite extends FunSuite {
case class Response(body: String, status: Int)
test("This test fails") {
val response = Response("this went wrong", 500)
expectOk(response) // The failure points here first
}
def expectOk(response: Response)(implicit loc: SourceLocation): Expectations = {
expectOkStatus(response)
.traced(here) // The failure points here third
}
def expectOkStatus(response: Response)(implicit loc: SourceLocation): Expectations = {
expect.eql(200, response.status)
.traced(here) // The failure points here second
}
}
Example report
repl.MdocSessionMdocAppTracingSuite - This test fails 2ms *************FAILURES************* repl.MdocSessionMdocAppTracingSuite - This test fails 2ms
Values not equal: (troubleshooting_failures.md:192)
(troubleshooting_failures.md:202)
(troubleshooting_failures.md:197)
in expect.eql(- expected, + found)
-200
+500
troubleshooting_failures.md:192
expectOk(response) // The failure points here first
^
troubleshooting_failures.md:202
.traced(here) // The failure points here second
^
troubleshooting_failures.md:197
.traced(here) // The failure points here third
^ Total 1, Failed 1, Passed 0, Ignored 0