Skip to content

Commit

Permalink
Improve ZIO Test Assertion Documentation (#8868)
Browse files Browse the repository at this point in the history
* initial work.

* add dependencies.

* multiple assertion inside assertTrue

* operations.

* what is assertion.

* initial work.

* multiple assertion inside assertTrue

* operations.

* what is assertion.

* initial work.

* fix negate section.

* reorder pages.

* remove "how it works" section.

* asserting nested values.

* type-checker macro.

* custom assertions.
  • Loading branch information
khajavi committed May 21, 2024
1 parent 78bf2a3 commit ee8c132
Show file tree
Hide file tree
Showing 8 changed files with 621 additions and 227 deletions.
34 changes: 18 additions & 16 deletions docs/reference/test/assertions/built-in-assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test("Fourth value is approximately equal to 5") {
}
```

### Any
## Any

Assertions that apply to `Any` value.

Expand All @@ -74,7 +74,7 @@ Assertions that apply to `Any` value.
| `nothing` | `Assertion[Any]` | Makes a new assertion that always fails. |
| `throwsA[E: ClassTag]` | `Assertion[Any]` | Makes a new assertion that requires the expression to throw. |

### A
## A

Assertions that apply to specific values.

Expand All @@ -86,7 +86,7 @@ Assertions that apply to specific values.
| `not[A](assertion: Assertion[A])` | `Assertion[A]` | Makes a new assertion that negates the specified assertion. |
| `throws[A](assertion: Assertion[Throwable])` | `Assertion[A]` | Makes a new assertion that requires the expression to throw. |

### Numeric
## Numeric

Assertions on `Numeric` types

Expand All @@ -99,7 +99,7 @@ Assertions on `Numeric` types
| `nonNegative[A](implicit num: Numeric[A])` | `Assertion[A]` | Makes a new assertion that requires a numeric value is non negative. |
| `nonPositive[A](implicit num: Numeric[A])` | `Assertion[A]` | Makes a new assertion that requires a numeric value is non positive. |

### Ordering
## Ordering

Assertions on types that support `Ordering`

Expand All @@ -111,7 +111,7 @@ Assertions on types that support `Ordering`
| `isLessThanEqualTo[A](reference: A)(implicit ord: Ordering[A])` | `Assertion[A]` | Makes a new assertion that requires the value be less than or equal to the specified reference value. |
| `isWithin[A](min: A, max: A)(implicit ord: Ordering[A])` | `Assertion[A]` | Makes a new assertion that requires a value to fall within a specified min and max (inclusive). |

### Iterable
## Iterable

Assertions on types that extend `Iterable`, like `List`, `Seq`, `Set`, `Map`, and many others.

Expand All @@ -135,7 +135,7 @@ Assertions on types that extend `Iterable`, like `List`, `Seq`, `Set`, `Map`, an
| `isEmpty` | `Assertion[Iterable[Any]]` | Makes a new assertion that requires an Iterable to be empty. |
| `isNonEmpty` | `Assertion[Iterable[Any]]` | Makes a new assertion that requires an Iterable to be non empty. |

### Ordering
## Ordering

Assertions that apply to ordered `Iterable`s

Expand All @@ -144,7 +144,7 @@ Assertions that apply to ordered `Iterable`s
| `isSorted[A](implicit ord: Ordering[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable is sorted. |
| `isSortedReverse[A](implicit ord: Ordering[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable is sorted in reverse order. |

### Seq
## Seq

Assertions that operate on sequences (`List`, `Vector`, `Map`, and many others)

Expand All @@ -154,7 +154,7 @@ Assertions that operate on sequences (`List`, `Vector`, `Map`, and many others)
| `hasAt[A](pos: Int)(assertion: Assertion[A])` | `Assertion[Seq[A]]` | Makes a new assertion that requires a sequence to contain an element satisfying the given assertion on the given position. |
| `startsWith[A](prefix: Seq[A])` | `Assertion[Seq[A]]` | Makes a new assertion that requires a given sequence to start with the specified prefix. |

### Either
## Either

Assertions for `Either` values.

Expand All @@ -165,7 +165,7 @@ Assertions for `Either` values.
| `isRight[A](assertion: Assertion[A])` | `Assertion[Either[Any, A]]` | Makes a new assertion that requires a Right value satisfying a specified assertion. |
| `isRight` | `Assertion[Either[Any, Any]]` | Makes a new assertion that requires an Either is Right. |

### Exit/Cause/Throwable
## Exit/Cause/Throwable

Assertions for `Exit` or `Cause` results.

Expand All @@ -180,7 +180,7 @@ Assertions for `Exit` or `Cause` results.
| `hasMessage(message: Assertion[String])` | `Assertion[Throwable]` | Makes a new assertion that requires an exception to have a certain message. |
| `hasThrowableCause(cause: Assertion[Throwable])` | `Assertion[Throwable]` | Makes a new assertion that requires an exception to have a certain cause. |

### Try
## Try

| Function | Result type | Description |
| -------- | ----------- | ----------- |
Expand All @@ -189,7 +189,7 @@ Assertions for `Exit` or `Cause` results.
| `isSuccess[A](assertion: Assertion[A])` | `Assertion[Try[A]]` | Makes a new assertion that requires a Success value satisfying the specified assertion. |
| `isSuccess` | `Assertion[Try[Any]]` | Makes a new assertion that requires a Try value is Success. |

### Sum type
## Sum type

An assertion that applies to some type, giving a method to transform the source
type into another type, then assert a property on that projected type.
Expand All @@ -199,7 +199,7 @@ type into another type, then assert a property on that projected type.
| `isCase[Sum, Proj]( termName: String, term: Sum => Option[Proj], assertion: Assertion[Proj])` | `Assertion[Sum]` | Makes a new assertion that requires the sum type be a specified term. |


### Map
## Map

Assertions for `Map[K, V]`

Expand All @@ -210,7 +210,7 @@ Assertions for `Map[K, V]`
| `hasKeys[K, V](assertion: Assertion[Iterable[K]])` | `Assertion[Map[K, V]]` | Makes a new assertion that requires a Map have keys satisfying the specified assertion. |
| `hasValues[K, V](assertion: Assertion[Iterable[V]])` | `Assertion[Map[K, V]]` | Makes a new assertion that requires a Map have values satisfying the specified assertion. |

### String
## String

Assertions for Strings

Expand All @@ -225,7 +225,7 @@ Assertions for Strings
| `matchesRegex(regex: String)` | `Assertion[String]` | Makes a new assertion that requires a given string to match the specified regular expression. |
| `startsWithString(prefix: String)` | `Assertion[String]` | Makes a new assertion that requires a given string to start with a specified prefix. |

### Boolean
## Boolean

Assertions for Booleans

Expand All @@ -234,7 +234,7 @@ Assertions for Booleans
| `isFalse` | `Assertion[Boolean]` | Makes a new assertion that requires a value be false. |
| `isTrue` | `Assertion[Boolean]` | Makes a new assertion that requires a value be true. |

### Option
## Option

Assertions for Optional values

Expand All @@ -244,10 +244,12 @@ Assertions for Optional values
| `isSome[A](assertion: Assertion[A])` | `Assertion[Option[A]]` | Makes a new assertion that requires a Some value satisfying the specified assertion. |
| `isSome` | `Assertion[Option[Any]]` | Makes a new assertion that requires an Option is Some. |

### Unit
## Unit

Assertion for Unit

| Function | Result type | Description |
| -------- | ----------- | ----------- |
| `isUnit` | `Assertion[Unit]` | Makes a new assertion that requires the value be unit. |


153 changes: 153 additions & 0 deletions docs/reference/test/assertions/classic-assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,156 @@ test("updating ref") {
}
```

## Understanding the `test` Function

:::note
In this section we are going to learn about the internals of the `Assertion` data type. So feel free to skip this section if you are not interested.
:::

In order to understand the `Assertion` data type, let's first look at the `test` function:

```scala
def test[In](label: String)(assertion: => In)(implicit testConstructor: TestConstructor[Nothing, In]): testConstructor.Out
```

Its signature is a bit complicated and uses _path-dependent types_, but it doesn't matter. We can think of a `test` as a function from `TestResult` (or its effectful versions such as `ZIO[R, E, TestResult]` or `ZSTM[R, E, TestResult]`) to the `Spec[R, E]` data type:

```scala
def test(label: String)(assertion: => TestResult): Spec[Any, Nothing]
def test(label: String)(assertion: => ZIO[R, E, TestResult]): Spec[R, E]
```

Therefore, the function `test` needs a `TestResult`. The most common way to produce a `TestResult` is to resort to `assert` or its effectful counterpart `assertZIO`. The former one is for creating ordinary `TestResult` values and the latter one is for producing effectful `TestResult` values. Both of them accept a value of type `A` (effectful version wrapped in a `ZIO`) and an `Assertion[A]`.

## Understanding the `assert` Function

Let's look at the `assert` function:

```scala
def assert[A](expr: => A)(assertion: Assertion[A]): TestResult
```

It takes an expression of type `A` and an `Assertion[A]` and returns the `TestResult` which is the boolean algebra of the `AssertionResult`. Furthermore, we have an `Assertion[A]` which is capable of producing _assertion results_ on any value of type `A`. So the `assert` function can apply the expression to the assertion and produce the `TestResult`.

## Type-checker Macro

To check if the code compiles, we can use the `typeCheck` macro. It is useful when we want to test if the code compiles without running it. Here is an example of how to use it:

```scala
import zio.test._
import zio.test.Assertion._

test("lazy list") {
assertZIO(typeCheck(
"""
|val lazyList: LazyList[Int] = LazyList(1, 2, 3, 4, 5)
|lazyList.foreach(println)
|""".stripMargin))(isRight)

} @@ TestAspect.exceptScala212
```

The `LazyCheck` introduced in Scala 2.13, so we excluded this test from Scala 2.12.

## Examples

### Example 1: Equality Assertion

Assume we have a function that concatenates two strings. One simple property of this function would be "the sum of the length of all inputs should be equal to the length of the output". Let's see an example of how we can make an assertion about this property:

```scala mdoc:compile-only
import zio.test._

test("The sum of the lengths of both inputs must equal the length of the output") {
check(Gen.string, Gen.string) { (a, b) =>
assert((a + b).length)(Assertion.equalTo(a.length + b.length))
}
}
```

The syntax of assertion in the above code, is `assert(expression)(assertion)`. The first section is an expression of type `A` which is _result_ of our computation and the second one is the expected assertion of type `Assertion[A]`.

### Example 2: Field-level Assertion

There is also an easy way to test an object's data for certain assertions with `hasField` which accepts besides a name, a mapping function from object to its tested property, and `Assertion` object which will validate this property. Here our test checks if a person has at least 18 years and is not from the USA.

```scala mdoc:reset-object:silent
import zio.test._
import zio.test.Assertion.{isRight, isSome,equalTo, isGreaterThanEqualTo, not, hasField}

final case class Address(country:String, city:String)
final case class User(name:String, age:Int, address: Address)

test("Rich checking") {
assert(
User("Jonny", 26, Address("Denmark", "Copenhagen"))
)(
hasField("age", (u:User) => u.age, isGreaterThanEqualTo(18)) &&
hasField("country", (u:User) => u.address.country, not(equalTo("USA")))
)
}
```

What is nice about those tests is that test reporters will tell you exactly which assertion was broken. Let's say we would change `isGreaterThanEqualTo(18)` to `isGreaterThanEqualTo(40)` which will fail. Print out on the console will be a nice detailed text explaining what exactly went wrong:

```bash
[info] User(Jonny,26,Address(Denmark,Copenhagen)) did not satisfy (hasField("age", _.age, isGreaterThanEqualTo(45)) && hasField("country", _.country, not(equalTo(USA))))
[info] 26 did not satisfy isGreaterThanEqualTo(45)
```

### Example 3: Test if a ZIO Effect Fails With a Particular Error Type

The following example shows how to test if a ZIO effect fails with a particular error type. To test if a ZIO effect fails with a particular error type, we can use the `ZIO#exit` to determine the exit type of that effect.

```scala mdoc:compile-only
import zio._
import zio.test.{ test, _ }
import zio.test.Assertion._

case class MyError(msg: String) extends Exception

val effect: ZIO[Any, MyError, Unit] = ZIO.fail(MyError("my error msg"))

test("test if a ZIO effect fails with a particular error type") {
for {
exit <- effect.exit
} yield assertTrue(exit == Exit.fail(MyError("my error msg")))
}
```

The exit method on a ZIO effect returns an `Exit` value, which represents the outcome of the effect. The `Exit` value can be either `Exit.succeed` or `Exit.fail`. If the effect succeeded, the `Exit.succeed` value will contain the result of the effect. If the effect failed, the `Exit.fail` value will contain the error that caused the failure.

### Example 4: Test if a ZIO Effect Fails With a Subtype of a Particular Error Type

To test if a ZIO effect fails with a `subtype` of a particular error type, we can use the `assertZIO` function and the two `fails`, and `isSubtype` assertions from the zio-test library. The `assertZIO` function takes a ZIO effect and an assertion. The assertion is called with the result of the ZIO effect. If the assertion returns true, then the `assertZIO` will succeed, otherwise it will fail.

Assume we have these error types:

```scala mdoc:silent
sealed trait MyError extends Exception
case class E1(msg: String) extends MyError
case class E2(msg: String) extends MyError
```

To assert if an error type is a subtype of a particular error type, we need to combine the `fails` and `isSubtype` assertions together:


```scala mdoc:compile-only
import zio.test.Assertion._

Assertion.fails(isSubtype[MyError](anything))
```

Now let's look at an example:

```scala mdoc:compile-only
import zio._
import zio.test.{ test, _ }
import zio.test.Assertion._

val effect = ZIO.fail(E1("my error msg"))

test("Test if a ZIO effect fails with a MyError") {
assertZIO(effect.exit)(fails(isSubtype[MyError](anything)))
}
```
Loading

0 comments on commit ee8c132

Please sign in to comment.