Meet the new kid
testing framework on the block
Petra Bierleutgeb
@pbvie
MUnit
presented at ScalaLove ❤️ Conf by
Great! ...but do we need one?
A new testing framework
ScalaTest
MUnit
scala-verify
uTest
minitest
🐲 The Dark Age without
Scala Testing Frameworks
specs2
Getting Started with MUnit
Hello World
libraryDependencies += "org.scalameta" %% "munit" % "0.7.2" % Test
testFrameworks += new TestFramework("munit.Framework")
class HelloMUnitSuite extends munit.FunSuite {
test("hello world") {
val greeting = "hello world"
assertEquals(greeting, "hello world")
}
}
clean and (quite) minimal testing suite that can be used without learning about the underlying implementation
comes with batteries included and should cover most of your
day-to-day testing needs :)
class HelloFunSuite extends munit.FunSuite {
def doSomething(): Int = ???
def doSomethingAsync(): Future[Int] ???
test("sync") {
doSomething()
}
test("async") {
doSomethingAsync()
}
}
automatically available when using FunSuite - can be mixed into your own suites
Tests as Values
abstract class Suite extends PlatformSuite {
// the value produced by test bodies
type TestValue
final type Test = GenericTest[TestValue]
// a suite consists of a sequence of tests
def munitTests(): Seq[Test]
...
a test is an instance of GenericTest[TestValue]
knowing about GenericTest[TestValue] is helpful to tweak/customize your tests but it's not required
using FunSuite will take care of most things
// simplified
abstract class FunSuite
extends Suite
with Assertions
with ... {
final type TestValue = Future[Any]
final val munitTestsBuffer: mutable.ListBuffer[Test] =
mutable.ListBuffer.empty[Test]
def munitTests(): Seq[Test] = {
munitSuiteTransform(munitTestsBuffer.toList)
}
def test(...) {
// create new test and add it to the test buffer
}
class GenericTest[T](
val name: String,
val body: () => T,
val tags: Set[Tag], // set of tags for filtering/transformations
val location: Location // enables "jump to test/error"
)
that encourages customization
Developer-friendly API
treating tests as values gives developers a lot of freedom
many customizations can be achieved through tags, filters and transformations
class CustomSuite extends munit.Suite {
override type TestValue = Future[String]
override def munitTests() = List(
new Test(
"my custom test",
// won't compile if test body is not a Future[String]
body = () => Future.successful("hello")
tags = Set.empty[Tag],
location = implicitly[Location]
)
)
}
Filtering and Transformation
with Tags
class TagSuite extends munit.FunSuite {
val takesForever = new munit.Tag("takesforever")
val db = new munit.Tag("db")
test("a very slow test".tag(takesForever).tag(db)) {
// ...
}
// ...
// always ignore suite
@munit.IgnoreSuite
class MySuite extends munit.FunSuite { ...
// ignore suite on dynamic condition
class MyWindowsOnlySuite extends munit.FunSuite {
override def munitIgnore: Boolean = // condition
// in your test suite
class TagSuite extends munit.FunSuite {
val db = new munit.Tag("db")
override def munitTests(): Seq[Test] = {
// useful for filtering based on dynamic conditions
val unfiltered = super.munitTests()
if (myCondition) unfiltered.filter(_.tags.contains(db))
else unfiltered
}
test("slow test".tag(db)) { ... }
// in sbt
testOnly -- --exclude-tags=db
or
used to transform suites/tests/values based on
static or dynamic conditions
SuiteTransforms
TestTransforms
ValueTransforms
import monix.eval.Task
// ...
class TaskSuite extends munit.FunSuite {
def getFromDbTask: Task[Int] = Task {
// pretend to get value from db
throw new RuntimeException("boom")
}
/* problem: Task is lazy, so nothing will actually run
and the test will not fail */
test("hello monix task") {
getFromDbTask.map { result =>
// ...
}
}
class TaskSuite extends munit.FunSuite {
def getFromDbTask: Task[Int] = Task {
// pretend to get value from db
throw new RuntimeException("boom")
}
/* problem: Task is lazy, so nothing will actually run
and the test will not fail */
test("hello monix task") {
getFromDbTask.map { result =>
// ...
}.runToFuture
}
class TaskSuite extends munit.FunSuite {
override def munitValueTransforms =
super.munitValueTransforms ++ List(
new ValueTransform("Task", {
case t: Task[_] => t.runToFuture
})
)
def getFromDbTask: Task[Int] = Task {
// pretend to get value from db
throw new RuntimeException("boom")
}
test("hello monix task") {
getFromDbTask.map { result =>
// ...
} // no need to call .runToFuture in every test anymore
}
and helpful diffs
Actionable Test Results
provide useful information to debug test failures right from the terminal/test output
final case class Cat(name: String, color: String)
test("diff case classes") {
val realGarfield = Cat("garfield", "ginger")
val imposterGarfield = Cat("garfield", "grey")
assertEquals(imposterGarfield, realGarfield)
}
...and more
and further reading
Credits
Hope you enjoyed it.
That's it for today!