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
Suiteet 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
- Choisir un style de test adapté au projet.
- Définir une classe de base commune.
- É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