diff --git a/build.sbt b/build.sbt index d1cfcfc..7a9059c 100644 --- a/build.sbt +++ b/build.sbt @@ -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(".")) @@ -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" +) diff --git a/munit-raise4s/README.md b/munit-raise4s/README.md new file mode 100644 index 0000000..cd64f3d --- /dev/null +++ b/munit-raise4s/README.md @@ -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) +
+ +# 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") +} +``` + diff --git a/munit-raise4s/src/main/scala/in/rcard/raise4s/munit/RaiseAssertions.scala b/munit-raise4s/src/main/scala/in/rcard/raise4s/munit/RaiseAssertions.scala new file mode 100644 index 0000000..b3ad0d4 --- /dev/null +++ b/munit-raise4s/src/main/scala/in/rcard/raise4s/munit/RaiseAssertions.scala @@ -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`. + * + *

Example

+ * {{{ + * 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) + } +} diff --git a/munit-raise4s/src/main/scala/in/rcard/raise4s/munit/RaiseSuite.scala b/munit-raise4s/src/main/scala/in/rcard/raise4s/munit/RaiseSuite.scala new file mode 100644 index 0000000..0d77271 --- /dev/null +++ b/munit-raise4s/src/main/scala/in/rcard/raise4s/munit/RaiseSuite.scala @@ -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.

Example

+ * {{{ + * 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.

Example

+ * {{{ + * 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 + ) + ) + } +} diff --git a/munit-raise4s/src/test/scala/RaiseSuiteSpec.scala b/munit-raise4s/src/test/scala/RaiseSuiteSpec.scala new file mode 100644 index 0000000..811a9fe --- /dev/null +++ b/munit-raise4s/src/test/scala/RaiseSuiteSpec.scala @@ -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" + ) + } +}