Smart Assertions
The smart assertion is a simple way to assert both ordinary values and ZIO effects. It uses the assertTrue
function, which uses macro under the hood.
Asserting Ordinary Values
In the following example, we assert simple ordinary values using the assertTrue
method:
import zio._
import zio.test.{test, _}
test("sum"){
assertTrue(1 + 1 == 2)
}
We can assert multiple assertions inside a single assertTrue
:
test("multiple assertions"){
assertTrue(
true,
1 + 1 == 2,
Some(1 + 1) == Some(2)
)
}
Asserting ZIO effects
The assertTrue
method can also be used to assert ZIO effects:
import zio._
import zio.test.{test, _}
test("updating ref") {
for {
r <- Ref.make(0)
_ <- r.update(_ + 1)
v <- r.get
} yield assertTrue(v == 1)
}
Using assertTrue
with for-comprehension style, we can think of testing as these three steps:
- Set up the test — In this section we should setup the system under test (e.g.
Ref.make(0)
). - Running the test — Then we run the test scenario according to the test specification. (e.g
ref.update(_ + 1)
) - Making assertions about the test - Finally, we should assert the result with the right expectations (e.g.
assertTrue(v == 1)
)
Assertion Operators
Each assertTrue
returns a AssertResult
, so they have the same operators as AssertResult
. Here are some of the useful operators:
&&
- This is the logical and operator to make sure that both assertions are true:
import zio.test._
test("&&") {
check(Gen.int <*> Gen.int) { case (x: Int, y: Int) =>
assertTrue(x + y == y + x) && assertTrue(x * y == y * x)
}
}
- || - This is the logical or operator to make sure that at least one of the assertions is true:
import zio.test._
suite("||")(
test("false || true") {
assertTrue(false) || assertTrue(true) // this will pass
},
test("true || false") {
assertTrue(true) || assertTrue(false) // this will pass
},
test("true || true") {
assertTrue(true) || assertTrue(true) // this will pass
},
test("false || false") {
assertTrue(false) || assertTrue(false) // this will false
},
)
!
- This is the logical not operator to negate the assertion:
import zio.test._
suite("unary !") (
test("negate true") {
!assertTrue(true) // this will fail
},
test("negate false") {
!assertTrue(false) // this will pass
}
)
- implies - This is the logical implies operator to make sure that the first assertion implies the second assertion. It is equivalent to
!p || q
which is a conditional statement of the form "if p, then q" where p and q are propositions. The==>
operator is an alias forimplies
.
import zio.test._
suite("implies") (
test("true implies true")(
assertTrue(true) implies assertTrue(true) // this will pass
),
test("true implies false")(
assertTrue(true) implies assertTrue(false) // this will fail
),
test("false implies true")(
assertTrue(false) implies assertTrue(true) // this will pass
),
test("false implies false")(
assertTrue(false) implies assertTrue(false) // this will pass
),
)
The implies
assertion is true if either the p is false or when both p and q are true:
P | Q | P implies Q |
---|---|---|
true | true | true |
true | false | false |
false | true | true |
false | false | true |
- iff - This is the logical iff operator to make sure that the first assertion is true if and only if the second assertion is true. It is equivalent to
(p implies q) && (q implies p)
. The<==>
operator is an alias foriff
.
import zio.test._
suite("iff") (
test("true iff true")(
assertTrue(true) iff assertTrue(true) // this will pass
),
test("true iff false")(
assertTrue(true) iff assertTrue(false) // this will fail
),
test("false iff true")(
assertTrue(false) iff assertTrue(true) // this will fail
),
test("false iff false")(
assertTrue(false) iff assertTrue(false) // this will pass
)
)
Here is the truth table for the iff operator:
P | Q | P iff Q |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | true |
- ??- We can add a custom message to the assertion using the
??
operator. This will be useful when assertion fails, and we want to provide more information about the failure:
import zio.test._
assertTrue(1 + 1 == 3) ?? "1 + 1 should be equal to 2"
Asserting Nested Values
There are several operators designed specifically for use within the assertTrue
macro, enhancing the ease and readability of assertions. These operators, intended exclusively for the assertTrue
macro, leverage the TestLens[A]
type-class to access the underlying value of the type A
.
We use the is
extension method inside the assertTrue
macro to convert the given value to a TestLens
. Now no matter how deeply nested the value is, we can access the underlying values using extension method defined for TestLens
values:
Testing Optional Values
There are two operators for testing optional values:
TestLens#some
- This operator is used to peek into theSome
value:
import zio.test._
test("optional value is some(42)") {
val sut: Option[Int] = Some(40 + 2)
assertTrue(sut.is(_.some) == 42)
}
TestLens#anything
- This operator is used to assert that the value isSome
:
import zio.test._
test("optional value is anything") {
val sut: Option[Int] = Some(42)
assertTrue(sut.is(_.anything))
}
Testing Either Values
TestLens#left
- This operator is used to peek into theLeft
value:
import zio.test._
test("TestLens#right") {
val sut: Either[Error, Int] = Right(40 + 2)
assertTrue(sut.is(_.right) == 42)
}
TestLens#left
- This operator is used to peek into theLeft
value:
import zio.test._
case class Error(errorMessage: String)
test("TestLens#left") {
val sut: Either[Error, Int] = Left(Error("Boom!"))
assertTrue(sut.is(_.left).errorMessage == "Boom!")
}
TestLens#anything
- This operator is used to assert that the value isRight
:
import zio.test._
test("TestLens#anything") {
val sut: Either[Error, Int] = Right(42)
assertTrue(sut.is(_.anything))
}
Testing Exit Values
TestLens#success
- This operator transforms theExit
value to its success typeA
if it is aExit.Success
, otherwise it will fail. So this can be used for asserting the success value of theExit
:
import zio.Exit
import zio.test._
test("TestLens#success") {
val sut: Exit[Error, Int] = Exit.succeed(42)
assertTrue(sut.is(_.success) == 42)
}
TestLens#failure
- This operator transforms theExit
value to its failure typeE
if it is aExit.Failure
, otherwise it will fail. So this can be used for asserting the failure value of theExit
:
import zio.Exit
import zio.test._
case class Error(errorMessage: String)
test("TestLens#failure") {
val sut: Exit[Error, Int] = Exit.fail(Error("Boom!"))
assertTrue(sut.is(_.failure).errorMessage == "Boom!")
}
TestLens#die
- This operator transforms theExit
value to its die typeE
if it is aExit.Die
, otherwise it will fail. So this can be used for asserting the die value of theExit
:
import zio.Exit
import zio.test._
test("TestLens#die") {
val sut: Exit[Error, Int] = Exit.die(new RuntimeException("Boom!"))
assertTrue(sut.is(_.die).getMessage == "Boom!")
}
TestLens#cause
- This operator transforms theExit
value to its underlyingCause
value if it has one otherwise it will fail. So this can be used for asserting the cause of theExit
:
import zio.{ZIO, Cause}
import zio.test._
test("TestLens#cause") {
for {
exit <- ZIO.failCause(Cause.fail("Boom!")).exit
} yield assertTrue(exit.is(_.cause) == Cause.fail("Boom!"))
}
// error: Error is already defined as case class Error
TestLens#interrupt
- This operator transforms theExit
value to its interrupt value if it is aExit.Interrupt
, otherwise it will fail. So this can be used for asserting the interrupt value of theExit
:
import zio.{durationInt, ZIO}
import zio.test._
test("TestLens#interrupt") {
for {
exit <- ZIO.sleep(5.seconds).fork.flatMap(_.interrupt)
} yield assertTrue(exit.is(_.interrupted))
}
Deeply Nested Values
Sometimes we need to test values with more than one level of nesting. There is no difference in the way we test nested values:
import zio.test._
test("assertion of multiple nested values (TestLens#right.some)") {
val sut: Either[Error, Option[Int]] = Right(Some(40 + 2))
assertTrue(sut.is(_.right.some) == 42)
}
Custom Assertions
Using CustomAssertion
we can create our own custom assertions for use in assertTrue
. We can define custom assertions using the CustomAssertion.make
method. This method takes a partial function from the type A
to Either[String, B]
. If the partial function is defined for the given value, it returns Right[B]
, otherwise it returns Left[String]
.
Here is an example of a custom assertion for a sealed trait and case classes:
import zio.test._
// Define the sealed trait and case classes
sealed trait Book
case class Novel(pageCount: Int) extends Book
case class Comic(illustrations: Int) extends Book
case class Textbook(subject: String) extends Book
// Custom assertion for Book
val subject =
CustomAssertion.make[Book] {
case Textbook(subject) => Right(subject)
case other => Left(s"Expected $$other to be Textbook")
}
// Usage
suite("custom assertions")(
test("subject assertion") {
val book: Option[Book] = Some(Textbook("Mathematics"))
assertTrue(book.is(_.some.custom(subject)) == "Mathematics")
}
)
In the above example, we define a custom assertion for the Book
sealed trait. The custom assertion subject
is defined to extract the subject
from the Textbook
case class. So then we can assert the subject
of the Textbook
case class.
More Examples
The assertTrue
macro is designed to make it easy to write assertions in a more readable way. Most test cases can be written as when we're comparing ordinary values in Scala. However, we have a SmartAssertionSpec
which is a collection of examples to demonstrate the power of the assertTrue
macro.