Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(87): Added integration with munit #117

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ lazy val `cats-raise4s` = project
.settings(
name := "cats-raise4s",
scalaVersion := scala3Version,
libraryDependencies ++= commonDependencies ++ `cats-raised4sDependencies`
libraryDependencies ++= commonDependencies ++ `cats-raise4sDependencies`
)

lazy val `munit-raise4s` = project
.dependsOn(core)
.settings(
name := "munit-raise4s",
scalaVersion := scala3Version,
libraryDependencies ++= commonDependencies ++ `munit-raise4sDependencies`
)

lazy val raise4s = (project in file("."))
Expand All @@ -59,6 +67,10 @@ lazy val commonDependencies = Seq(
dependencies.scalatest % Test
)

lazy val `cats-raised4sDependencies` = Seq(
lazy val `cats-raise4sDependencies` = Seq(
"org.typelevel" %% "cats-core" % "2.12.0"
)

lazy val `munit-raise4sDependencies` = Seq(
"org.scalameta" %% "munit" % "1.0.3"
)
64 changes: 64 additions & 0 deletions munit-raise4s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/rcardin/raise4s/scala.yml?branch=main)
![Maven Central](https://img.shields.io/maven-central/v/in.rcard.raise4s/munit-raise4s_3)
[![javadoc](https://javadoc.io/badge2/in.rcard.raise4s/muint-raise4s_3/javadoc.svg)](https://javadoc.io/doc/in.rcard.raise4s/munit-raise4s_3)
<br/>

# MUnit Integration for Raise4s

Integration of the Raise DSL with the MUnit testing framework.

## Dependency

The library is available on Maven Central. To use it, add the following dependency to your `build.sbt` files:

```sbt
libraryDependencies += "in.rcard.raise4s" %% "munit-raise4s" % "0.4.0"
```

The library is only available for Scala 3.

## Usage

The library let you test functions that can raise errors with the `munit` testing framework. Please refer to the [mUnit documentation](https://scalameta.org/munit/) for more general information about the testing framework.

To declare a new test, use the `testR` method available through the `RaiseSuite` test suite. The method accepts a body that can raise errors. For example:

```scala 3
import in.rcard.raise4s.munit.RaiseSuite

class RaiseSuiteSpec extends RaiseSuite {
testR("A test should succeed on successful Raise instances") {
val one: Int raises String = 1
val two: Int raises String = 2
assert(one < two)
}
}
```

If the body raises an error, the test will fail with the error message. For example, the following test fails:

```scala 3
testR("A test should fail on failed Raise instances") {
val one: Int raises String = Raise.raise("An error occurred")
val two: Int raises String = 2
assert(one < two)
}
```

The message we receive is:

```
munit.FailException: Expected the test not to raise any errors but it did with error 'An error occurred'
```

If you want to test if a block raises an error and you want to assert some properties on the error, you can use the `in.rcard.raise4s.munit.RaiseAssertions.interceptR` method:

```scala 3
testR("intercept assertion should succeed on a raised error") {
val error: String = interceptR[String] {
Raise.raise("An error occurred")
}
assertEquals(error, "An error occurred")
}
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package in.rcard.raise4s.munit

import in.rcard.raise4s.Raise
import munit.Assertions.fail
import munit.Location

import scala.reflect.ClassTag

/** Set of assertions for tests that can raise errors.
*/
trait RaiseAssertions {

/** Evaluates the given expression and asserts that the expected raised error is of type `T`.
*
* <h2>Example</h2>
* {{{
* val error: String = interceptR[String] {
* Raise.raise("An error occurred")
* }
* assertEquals(error, "An error occurred")
* }}}
*
* @param body
* The function that should raise an error
* @tparam Error
* The type of the error to intercept
* @return
* The raised error
*/
def interceptR[Error](
body: Raise[Error] ?=> Any
)(implicit ErrorClass: ClassTag[Error], loc: Location): Error = {
val result: Error | Any = Raise.run {
body
}
result match
case error: Error => error
case _ =>
val expectedExceptionMsg =
s"Expected error of type '${ErrorClass.runtimeClass.getName}' but body evaluated successfully"
fail(expectedExceptionMsg)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package in.rcard.raise4s.munit

import in.rcard.raise4s.Raise
import munit.{FunSuite, Location, TestOptions}

/** Suite that provides a test methods that can handle computations that can raise errors.
*/
abstract class RaiseSuite extends FunSuite with RaiseAssertions {

/** A test for computations that can raise errors. <h2>Example</h2>
* {{{
* testR("A test should succeed on successful Raise instances") {
* val one: Int raises String = 1
* val two: Int raises String = 2
* assert(one < two)
* }
* }}}
*
* @tparam Error
* The type of the error that can be raised
*/
def testR[Error](name: String)(body: Raise[Error] ?=> Unit)(implicit loc: Location): Unit = {
test(new TestOptions(name))(
Raise.fold(
block = body,
recover = error =>
fail(s"Expected the test not to raise any errors but it did with error '$error'"),
transform = identity
)
)
}

/** A test for computations that can raise errors and that can be configured with the given
* options. <h2>Example</h2>
* {{{
* testR("A test should fail on failed Raise instances".fail) {
* val one: Int raises String = Raise.raise("An error occurred")
* val two: Int raises String = 2
* assert(one < two)
* }
* }}}
*
* @tparam Error
* The type of the error that can be raised
*/
def testR[Error](
options: TestOptions
)(body: Raise[Error] ?=> Unit)(implicit loc: Location): Unit = {
test(options)(
Raise.fold(
block = body,
recover = error =>
fail(s"Expected the test not to raise any errors but it did with error '$error'"),
transform = identity
)
)
}
}
38 changes: 38 additions & 0 deletions munit-raise4s/src/test/scala/RaiseSuiteSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import in.rcard.raise4s.munit.RaiseSuite
import in.rcard.raise4s.{Raise, raises}
import munit.FailException

class RaiseSuiteSpec extends RaiseSuite {
testR("A test should succeed on successful Raise instances") {
val one: Int raises String = 1
val two: Int raises String = 2
assert(one < two)
}

testR("A test should fail on failed Raise instances".fail) {
val one: Int raises String = Raise.raise("An error occurred")
val two: Int raises String = 2
assert(one < two)
}

testR("intercept assertion should succeed on a raised error") {
val error: String = interceptR[String] {
Raise.raise("An error occurred")
}

assertEquals(error, "An error occurred")
}

testR("intercept assertion should fail on a successful Raise instance") {
val actualException = intercept[FailException] {
interceptR[String] {
42
}
}

assertEquals(
actualException.getMessage,
"Expected error of type 'java.lang.String' but body evaluated successfully"
)
}
}
Loading