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
Scala Testing Frameworks
through the ages
specs2
# Quick Facts
- started by Ólafur Páll Geirsson - @olafurpg
- first release in 2020
- used in several Scalameta projects
- member of the ScalaMeta family
# Goals and Features I
- tests as values
- minimal and developer-friendly API
- extensive and dynamic filtering
- transformations
- actionable errors
# Goals and Features II
- support for the whole Scala ecosystem
- good IDE support, leverage existing tooling
- visibility and reporting
- support for ScalaCheck
(contributed by Gabriele Petronella - @gabro27)
Getting Started with MUnit
Hello World
# Setup
libraryDependencies += "org.scalameta" %% "munit" % "0.7.2" % Test
testFrameworks += new TestFramework("munit.Framework")
# Writing tests
class HelloMUnitSuite extends munit.FunSuite {
test("hello world") {
val greeting = "hello world"
assertEquals(greeting, "hello world")
}
}
- test suites must be classes
- extend munit.Suite
- use munit.FunSuite for convenience...or roll your own
# FunSuite
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 :)
# FunSuite
- best way to get started with MUnit
- provides `test` method to define tests that automatically work with sync/async values
class HelloFunSuite extends munit.FunSuite {
def doSomething(): Int = ???
def doSomethingAsync(): Future[Int] ???
test("sync") {
doSomething()
}
test("async") {
doSomethingAsync()
}
}
# Features as traits
- assertions
- fixtures, before/after
- transformations for suites, tests and values
automatically available when using FunSuite - can be mixed into your own suites
Tests as Values
# 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
# Tests as Values - FunSuite
// 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
}
# GenericTest[TestValue]
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
# Customizations
treating tests as values gives developers a lot of freedom
many customizations can be achieved through tags, filters and transformations
# munit.Suite - the minimalist
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
# How to define 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)) {
// ...
}
// ...
# Filtering - Suites
// 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
# Filtering - Tests
// 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
# Transformations
used to transform suites/tests/values based on
static or dynamic conditions
# Transformations
SuiteTransforms
TestTransforms
ValueTransforms
# Example: Value Transformation
- adding support for monix Task
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 =>
// ...
}
}
# Example: Value Transformation
- one way to fix it
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
}
# Example: Value Transformation
- but it's not nice: we have to repeat it in every single test
- even worse: if we forget it, the test will still pass
- a better solution: automatically transform the Task to a Future by using a Value Transformation
# Example: Value Transformation
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
# Actionable Test Results
provide useful information to debug test failures right from the terminal/test output
# Actionable Test Results
- jump to location
- clues
- stracktrace highlighting
- nice diffs when using `assertEquals`
# Actionable Test Results
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)
}
# Actionable Test Results
...and more
# Features/Content that didn't make it
into the presentation
- fixtures
- special treatment of flaky tests
- sbt plugin to upload reports to Google Cloud Storage and generate reports with mdoc
- more examples
and further reading
Credits
# MUnit is inspired by
- ScalaTest
- uTest
- JUnit
- ava (JavaScript test framework)
# Reading Material and references
Hope you enjoyed it.
That's it for today!
Meet MUnit
By Petra Bierleutgeb
Meet MUnit
@ ScalaLove conf 2020
- 1,674