scalatutorial.de

Stufe 6Werkzeuge und Oberflächen · Lektion 30 von 34

Testen mit ScalaTest, MUnit und ScalaCheck

Aktualisiert am 24. September 20263 Min. LesezeitSchritt 2/6 dieser Stufe

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:

src/main/scala/Preise.scalaScala 3
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.

Testkreislauf mit sbt: ändern, testen, grün oder rotCode ändernsbt ~testgrünrotMeldung lesen, korrigieren, erneut laufen lassen
Der Testkreislauf: ändern, sbt ~test, Meldung lesen.

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:

build.sbtsbt
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test

Der einfachste Stil ist AnyFunSuite:

src/test/scala/PreiseSuite.scalaScala 3
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:

 Scala 3
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:

build.sbtsbt
libraryDependencies += "org.scalameta" %% "munit" % "1.0.0" % Test
src/test/scala/PreiseTest.scalaScala 3
class 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.

 Scala 3
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

 Terminal
$ sbt test                          # alle Tests
$ sbt "testOnly PreiseSuite"        # eine Klasse
$ sbt ~testQuick                    # bei jeder Änderung nur Betroffenes
$ scala test .                      # ohne sbt, mit Scala CLI

Was gute Tests ausmacht

Passt dazu