Stufe 6Werkzeuge und Oberflächen · Lektion 30 von 34
Testen mit ScalaTest, MUnit und ScalaCheck
Tests sind in Scala-Projekten selbstverständlich – und dank reiner Funktionen oft angenehm einfach. Drei Bibliotheken prägen die Landschaft: ScalaTest mit vielen Stilen, MUnit als schlanke Alternative und ScalaCheck für eigenschaftsbasierte Tests.
Was getestet wird
Als Beispiel dient eine kleine Funktion, die aus einem Nettobetrag den Bruttobetrag berechnet und auf Cent rundet:
object Preise:
def brutto(netto: BigDecimal, satz: BigDecimal = BigDecimal("0.19")): BigDecimal =
require(netto >= 0, "Netto darf nicht negativ sein")
(netto * (BigDecimal(1) + satz)).setScale(2, BigDecimal.RoundingMode.HALF_UP)Tests liegen in sbt-Projekten unter src/test/scala und werden mit sbt test ausgeführt.
ScalaTest
ScalaTest ist die älteste und umfangreichste Testbibliothek. Sie bietet mehrere Stile, von schlichten Testfunktionen bis zu verhaltensorientierten Beschreibungen. Eingebunden wird sie in der build.sbt:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % TestDer einfachste Stil ist AnyFunSuite:
import org.scalatest.funsuite.AnyFunSuite
class PreiseSuite extends AnyFunSuite:
test("19 Prozent auf 10 Euro"):
assert(Preise.brutto(10) == BigDecimal("11.90"))
test("rundet kaufmännisch"):
assert(Preise.brutto(BigDecimal("19.99")) == BigDecimal("23.79"))
test("lehnt negative Beträge ab"):
assertThrows[IllegalArgumentException]:
Preise.brutto(-1)Wer Tests lieber als Sätze liest, nimmt AnyFlatSpec mit den Matchern:
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class PreiseSpec extends AnyFlatSpec with Matchers:
"brutto" should "den ermäßigten Satz berücksichtigen" in {
Preise.brutto(100, BigDecimal("0.07")) shouldBe BigDecimal("107.00")
}Schlägt ein Test fehl, zeigt ScalaTest beide Werte an – bei assert sogar den ganzen Ausdruck mit Zwischenwerten.
MUnit
MUnit ist schlanker, startet schneller und hat besonders gut lesbare Fehlermeldungen mit farbigen Unterschieden. Es ist die Vorlage in vielen Scala-3-Projektvorlagen:
libraryDependencies += "org.scalameta" %% "munit" % "1.0.0" % Testclass PreiseTest extends munit.FunSuite:
test("19 Prozent auf 10 Euro") {
assertEquals(Preise.brutto(10), BigDecimal("11.90"))
}
test("0 bleibt 0") {
assertEquals(Preise.brutto(0), BigDecimal("0.00"))
}Welche Bibliothek Sie wählen, ist Geschmackssache. Die Grundideen – ein Test pro Aussage, klare Namen, erwarteter Wert gegen tatsächlichen – sind überall gleich.
ScalaCheck: Eigenschaften statt Beispiele
Beispieltests prüfen einzelne Fälle, die sich jemand ausgedacht hat. Eigenschaftsbasierte Tests gehen anders vor: Sie formulieren eine Regel, die für alle Eingaben gelten soll, und ScalaCheck erzeugt Hunderte zufällige Eingaben, um sie zu widerlegen.
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll
object ListenEigenschaften extends Properties("List"):
property("zweimal umkehren ergibt das Original") = forAll { (xs: List[Int]) =>
xs.reverse.reverse == xs
}
property("Länge bleibt beim Sortieren gleich") = forAll { (xs: List[Int]) =>
xs.sorted.length == xs.length
}
property("brutto ist nie kleiner als netto") = forAll { (cent: Int) =>
val netto = BigDecimal(math.abs(cent.toLong)) / 100
Preise.brutto(netto) >= netto
}Findet ScalaCheck ein Gegenbeispiel, schrumpft es die Eingabe auf den kleinsten Fall, der die Eigenschaft noch verletzt – etwa eine Liste mit einem einzigen Element. Das macht die Fehlersuche erstaunlich schnell. ScalaCheck lässt sich einzeln verwenden oder in ScalaTest und MUnit einbinden (scalatestplus-scalacheck bzw. munit-scalacheck).
Tests ausführen
$ sbt test # alle Tests
$ sbt "testOnly PreiseSuite" # eine Klasse
$ sbt ~testQuick # bei jeder Änderung nur Betroffenes
$ scala test . # ohne sbt, mit Scala CLIWas gute Tests ausmacht
- Reine Funktionen testen sich fast von selbst. Je mehr Logik in Funktionen ohne Nebenwirkungen steckt, desto weniger Vorbereitung brauchen Tests (siehe funktional denken).
- Fehlerfälle gehören dazu. Wer mit Either arbeitet, testet die Left-Fälle genauso wie die Right-Fälle.
- Ein Test, eine Aussage. Scheitert ein Test, soll der Name schon sagen, was kaputt ist.
- Keine Zufallswerte ohne Kontrolle. Zufall gehört in eigenschaftsbasierte Tests, wo die Bibliothek ihn reproduzierbar macht – nicht in Beispieltests.