Integration library between MUnit and ZIO.
Library published for Scala 3, 2.13, 2.12.
libraryDependencies += "org.scalameta" %% "munit" % munitVersion % Test
libraryDependencies += "com.github.poslegm" %% "munit-zio" % munitZIOVersion % Test
If you are using a version of sbt lower than 1.5.0, you will also need to add:
testFrameworks += new TestFramework("munit.Framework")
Run tests:
$ sbt
> test
> testOnly com.MySuite
Full MUnit documentation available here.
MUnit integration provided by abstract class ZSuite
. It contains testZ
function which runs ZIO
effect as MUnit test.
Note:
ZSuite
will throwWrongTestMethodError
if you passZIO
into plain MUnittest
method. It prevents non-running effects in tests. Also compiler option-Wvalue-discard
is highly recommended for working with effects.
import munit.*
import zio.*
class SimpleZIOSpec extends ZSuite:
testZ("1 + 1 = 2") {
for
a <- ZIO.attempt(1)
b <- ZIO.attempt(1)
yield assertEquals(a + b, 2)
}
You can use any of MUnit assertions in ZIO
code, but ZSuite
contains
helpful ZIO
-specific assertions.
Asserts that ZIO[R, E, Boolean]
returns true
.
testZ("false OR true should be true") {
val effect = ZIO.succeed(false || true)
assertZ(effect)
}
Asserts that ZIO[R, E, String]
has no difference with expected string. Pretty
prints diff unlike just assertEqualsZ
.
testZ("strings are the same") {
val effect = ZIO.succeed("string")
assertNoDiffZ(effect, "string")
}
Asserts that ZIO[R, E, A]
returns the same result as expected
testZ("values are the same") {
val effect = ZIO.succeed(42)
assertEqualsZ(effect, 42)
}
Extension methods for intercept ZIO
's failures or defects.
Asserts that ZIO[R, E, Any]
should fail with provided exception E
.
testZ("effect should fail") {
val effect = ZIO.fail(new IllegalArgumentException("BOOM!"))
effect.interceptFailure[IllegalArgumentException]
}
Asserts that ZIO[R, E, Any]
should fail with provided exception E
and
message message
.
testZ("effect should fail with message") {
val effect = ZIO.fail(new IllegalArgumentException("BOOM!"))
effect.interceptFailureMessage[IllegalArgumentException]("BOOM!")
}
Asserts that ZIO[R, E, Any]
should die with provided exception E
.
testZ("effect should die") {
val effect = ZIO.die(new IllegalArgumentException("BOOM!"))
effect.interceptDefect[IllegalArgumentException]
}
Resource management in ZSuite based on Scoped
and MUnit fixtures.
"Test-local" means that resource will be acquired and released on every
testZ
execution. "Suite-local" means that resource will be acquired before
all testZ
executions and released after all testZ
executions.
Resources from test- and suite-local fixtures can be accessed directly from
testZ
.
You can create test-local fixture from Scoped
or raw acquire/release effects.
// define fixture with raw acquire/release effects
// `options` contains metadata about current test like its name
val rawZIOFunFixture = ZTestLocalFixture(options => ZIO.succeed(s"acquired ${options.name}")) {
str => putStrLn(s"cleanup [$str]").provideLayer(Console.live)
}
// use it with `testZ` extension method with resource access
rawZIOFunFixture.testZ("allocate resource with ZIO FunFixture") { str => // <- resource
val effect = ZIO.attempt(str.trim)
assertNoDiffZ(effect, "acquired allocate resource with ZIO FunFixture")
}
// similarly for `Scoped`
val ScopedFunFixture = ZTestLocalFixture { options =>
ZIO.acquireRelease(ZIO.succeed(s"acquired ${options.name} with Scoped")) { str =>
printLine(s"cleanup [$str] with Scoped").orDie
}
}
ScopedFunFixture.testZ("allocate resource with Scoped FunFixture") { str =>
val effect = ZIO.attempt(str.trim)
assertNoDiffZ(effect, "acquired allocate resource with Scoped FunFixture with Scoped")
}
Suite-local fixture can be created from Scoped
and provides synchronous
access to its resource.
// dirty example, don't do it in real code
var state = 0
val fixture = ZSuiteLocalFixture(
"sample",
ZIO.acquireRelease(ZIO.attempt { state += 1; state })(_ => ZIO.attempt { state += 1 }.orDie)
)
// suite-local fixture should be necessarily initialized
override val munitFixtures = Seq(fixture)
test("suite local fixture works") {
val current: Int = fixture() // <- access resource
assertEquals(current, 1)
}
You can provide dependencies to your tests with test-local fixture. There is
testZLayered
extension method for fixtures with ULayer[R]
resource. It just
provides layers into test body, so ZIO[R, E, A]
can be converted to ZIO[Any, E, A]
with ULayer[R]
fixture.
// accessor methods dependent on some `StatefulRepository` and `Service`:
def clean: URIO[Has[StatefulRepository], Unit] = ZIO.service[StatefulRepository].flatMap(_.clean)
def fetch: RIO[Has[Service], Unit] = ZIO.service[Service].flatMap(_.fetch)
def write: RIO[Has[Service], Unit] = ZIO.service[Service].flatMap(_.write)
// ===========
val layersFixture = ZTestLocalFixture { _ =>
// wire layers in `Scoped`'s acquire
ZIO.acquireRelease(ZIO.succeed(StatefulRepository.test >+> Service.test))(layers =>
// graceful release resources after test execution
clean.provideLayer(layers)
)
}
layersFixture.testZLayered("auto provide layer") {
val effect: RIO[Has[Service], Unit] = write *> write *> fetch
assertEqualsZ(effect, 2) // Has[Service] will be provided from fixture
}