Guide pratique pour écrire des tests avec ScalaTest

Fondamentaux de ScalaTest

ScalaTest est un framework de tests modulaire qui propose plusieurs styles d'écriture. Il repose sur quelques idées simples :

  • Une suite, représentée par le trait Suite, est un ensemble de tests.
  • Un test possède un nom et peut réussir, échouer, être en attente ou être annulé.
  • Les style traits étendent Suite et proposent des syntaxes différentes.
  • Les mixin traits enrichissent ces styles pour répondre à des besoins spécifiques.
  • On combine ces traits pour définir des classes de test adaptées à chaque projet.

Ajouter ScalaTest au projet

Avec sbt, ajoutez la dépendance suivante dans build.sbt :

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % Test

Avec Maven, utilisez la configuration suivante :

<dependency>
  <groupId>org.scalatest</groupId>
  <artifactId>scalatest_3</artifactId>
  <version>3.2.17</version>
  <scope>test</scope>
</dependency>

Workflow en trois étapes

  1. Choisir un style de test adapté au projet.
  2. Définir une classe de base commune.
  3. Écrire et exécuter les tests.

Étape 1 : choisir un style de test

Il est recommandé de sélectionner un style principal pour les tests unitaires et d'intégration, et un autre pour les tests d'acceptation. Le style FlatSpec est une valeur sûre par défaut, car il reste proche de xUnit tout en encourageant des noms de tests explicites.

FunSuite

Style familier pour ceux venant de xUnit, avec la possibilité de donner facilement des noms explicites aux tests.

import org.scalatest.funsuite.AnyFunSuite

class CalculatorSuite extends AnyFunSuite {

  private val calc = new Calculator

  test("addition returns the correct sum") {
    assert(calc.add(2, 3) == 5)
  }

  test("division by zero throws an exception") {
    assertThrows[IllegalArgumentException] {
      calc.divide(10, 0)
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-funsuite" % "3.2.17" % Test

FlatSpec

Style recommandé par défaut, proche de xUnit mais avec des phrases du type « X should Y ».

import org.scalatest.flatspec.AnyFlatSpec

class CalculatorSpec extends AnyFlatSpec {

  "A Calculator" should "return the correct sum" in {
    val calc = new Calculator
    assert(calc.add(4, 5) == 9)
  }

  it should "reject division by zero" in {
    assertThrows[IllegalArgumentException] {
      new Calculator().divide(5, 0)
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-flatspec" % "3.2.17" % Test

FunSpec

Style orienté BDD avec des blocs describe et it, inspiré de RSpec.

import org.scalatest.funspec.AnyFunSpec

class CalculatorSpec extends AnyFunSpec {

  describe("A Calculator") {
    describe("when computing a sum") {
      it("should return the expected result") {
        assert(new Calculator().add(3, 7) == 10)
      }
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-funspec" % "3.2.17" % Test

WordSpec

Style proche de specs/specs2, particulièrement adapté aux équipes souhaitant un texte très structuré.

import org.scalatest.wordspec.AnyWordSpec

class CalculatorSpec extends AnyWordSpec {

  "A Calculator" when {
    "adding two numbers" should {
      "return the correct result" in {
        assert(new Calculator().add(6, 3) == 9)
      }
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-wordspec" % "3.2.17" % Test

FreeSpec

Style très libre, adapté aux équipes expérimentées en BDD qui maîtrisent leur propre organisation textuelle.

import org.scalatest.freespec.AnyFreeSpec

class CalculatorSpec extends AnyFreeSpec {

  "A Calculator" - {
    "when adding two integers" - {
      "should return the correct value" in {
        assert(new Calculator().add(1, 2) == 3)
      }
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-freespec" % "3.2.17" % Test

PropSpec

Style orienté tests basés sur des propriétés, utile pour valider un comportement sur de nombreux cas.

import org.scalatest.propspec.AnyPropSpec
import org.scalatest.prop.{Table, TableDrivenPropertyChecks}
import org.scalatest.matchers.should.Matchers

class AdditionSpec extends AnyPropSpec with TableDrivenPropertyChecks with Matchers {

  private val cases = Table(
    ("left", "right", "expected"),
    (1, 2, 3),
    (0, 0, 0),
    (-4, 4, 0)
  )

  property("addition produces the expected result") {
    forAll(cases) { (left, right, expected) =>
      left + right should be(expected)
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-propspec" % "3.2.17" % Test

FeatureSpec

Style destiné aux tests d'acceptation, facilitant l'epxression des exigences avec Given, When et Then.

import org.scalatest.featurespec.AnyFeatureSpec
import org.scalatest.GivenWhenThen

class BankAccount {
  private var balance: Int = 0
  def deposit(amount: Int): Unit = balance += amount
  def currentBalance: Int = balance
}

class AccountFeatureSpec extends AnyFeatureSpec with GivenWhenThen {

  info("As a bank customer")
  info("I want to deposit money")
  info("So that my balance increases")

  feature("Deposit") {
    scenario("Customer makes a deposit") {

      Given("an account with a zero balance")
      val account = new BankAccount

      When("the customer deposits 100")
      account.deposit(100)

      Then("the balance should be 100")
      assert(account.currentBalance == 100)
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-featurespec" % "3.2.17" % Test

RefSpec

Style permettant de définir les tests comme des méthodes, ce qui réduit le nombre de classes et de fermetures générées. Il repose sur la réflexion et n'est pas disponible avec Scala.js.

import org.scalatest.refspec.RefSpec

class ListSpec extends RefSpec {

  object `A List` {
    object `when empty` {
      def `should have length 0` {
        assert(List.empty[Int].length == 0)
      }

      def `should throw NoSuchElementException when head is called` {
        assertThrows[NoSuchElementException] {
          List.empty[Int].head
        }
      }
    }
  }
}

libraryDependencies += "org.scalatest" %% "scalatest-refspec" % "3.2.17" % Test

Étape 2 : définir une classe de base

Au lieu de répéter les mêmes imports et traits dans chaque classe de test, il est recommandé de centraliser ces éléments dans une classe abstraite.

package com.example.myproject

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.{OptionValues, Inside, Inspectors}

abstract class UnitSpec extends AnyFlatSpec with Matchers
  with OptionValues with Inside with Inspectors

Les tests du projet peuvent alors hériter de cette classe de base :

package com.example.myproject

class MySpec extends UnitSpec {
  // tests
}

Étape 3 : écrire et exécuter un premier test

import org.scalatest.flatspec.AnyFlatSpec

class Calculator {
  def add(a: Int, b: Int): Int = a + b
}

class FirstSpec extends AnyFlatSpec {

  "A simple calculator" should "compute a basic addition" in {
    val calc = new Calculator
    assert(calc.add(3, 4) == 7)
  }
}

Placez le fichier dans src/test/scala/FirstSpec.scala, puis exécutez les tests avec sbt :

sbt test

Étiquettes: ScalaTest Scala sbt Maven FlatSpec

Publié le 17 juillet à 07h11