scalaintermediate

Testing with Specs2 Framework

Write readable tests with specs2: acceptance specs, matchers, data tables, and mock integration.

scala
import org.specs2.mutable.Specification
import org.specs2.specification.core.SpecStructure

// Unit under test
class Calculator:
  def add(a: Int, b: Int): Int = a + b
  def divide(a: Int, b: Int): Either[String, Double] =
    if b == 0 then Left("Division by zero")
    else Right(a.toDouble / b)
  def fibonacci(n: Int): Int =
    if n <= 1 then n
    else fibonacci(n - 1) + fibonacci(n - 2)

class StringUtils:
  def capitalize(s: String): String =
    if s.isEmpty then s
    else s.head.toUpper + s.tail.toLowerCase

  def isPalindrome(s: String): Boolean =
    val clean = s.toLowerCase.filter(_.isLetterOrDigit)
    clean == clean.reverse

  def wordCount(s: String): Map[String, Int] =
    s.toLowerCase.split("\\s+")
      .filter(_.nonEmpty)
      .groupBy(identity)
      .view.mapValues(_.length).toMap

// Mutable specification (xUnit-style)
class CalculatorSpec extends Specification:
  "Calculator" >> {
    val calc = Calculator()

    "add" >> {
      "add two positive numbers" >> {
        calc.add(2, 3) must_== 5
      }
      "add negative numbers" >> {
        calc.add(-1, -2) must_== -3
      }
      "add zero" >> {
        calc.add(0, 5) must_== 5
      }
    }

    "divide" >> {
      "divide normally" >> {
        calc.divide(10, 3) must beRight(beCloseTo(3.33 +/- 0.01))
      }
      "return error for zero divisor" >> {
        calc.divide(10, 0) must beLeft("Division by zero")
      }
    }

    "fibonacci" >> {
      "return 0 for 0" >> { calc.fibonacci(0) must_== 0 }
      "return 1 for 1" >> { calc.fibonacci(1) must_== 1 }
      "return 8 for 6" >> { calc.fibonacci(6) must_== 8 }
    }
  }

// String utils spec with matchers
class StringUtilsSpec extends Specification:
  "StringUtils" >> {
    val utils = StringUtils()

    "capitalize" >> {
      "capitalize first letter" >> {
        utils.capitalize("hello") must_== "Hello"
      }
      "handle empty string" >> {
        utils.capitalize("") must beEmpty
      }
      "handle single char" >> {
        utils.capitalize("a") must_== "A"
      }
      "lowercase rest" >> {
        utils.capitalize("hELLO") must_== "Hello"
      }
    }

    "isPalindrome" >> {
      "detect palindrome" >> {
        utils.isPalindrome("racecar") must beTrue
      }
      "ignore case" >> {
        utils.isPalindrome("RaceCar") must beTrue
      }
      "ignore non-alphanumeric" >> {
        utils.isPalindrome("A man, a plan, a canal: Panama") must beTrue
      }
      "reject non-palindrome" >> {
        utils.isPalindrome("hello") must beFalse
      }
    }

    "wordCount" >> {
      "count words" >> {
        utils.wordCount("hello world hello") must_== Map(
          "hello" -> 2,
          "world" -> 1
        )
      }
      "handle empty" >> {
        utils.wordCount("") must beEmpty
      }
    }
  }

// Matchers showcase
class MatchersSpec extends Specification:
  "Matchers" >> {
    // Equality
    "equality" >> { 1 + 1 must_== 2 }

    // Comparison
    "comparison" >> {
      5 must beGreaterThan(3)
      2 must beLessThanOrEqualTo(2)
      3.14 must beCloseTo(3.14 +/- 0.001)
    }

    // String matchers
    "strings" >> {
      "hello world" must contain("world")
      "hello" must startWith("hel")
      "hello" must endWith("llo")
      "hello" must have length 5
      "hello" must beMatching("h.*o")
    }

    // Collection matchers
    "collections" >> {
      List(1, 2, 3) must contain(2)
      List(1, 2, 3) must have size 3
      List(1, 2, 3) must containAllOf(List(1, 3))
      List(3, 1, 2) must beSorted.not
      List.empty[Int] must beEmpty
    }

    // Option/Either
    "option" >> {
      Some(42) must beSome(42)
      None must beNone
    }

    // Exception
    "exception" >> {
      { throw RuntimeException("boom") } must throwA[RuntimeException](message = "boom")
    }
  }

Use Cases

  • Behavior-driven development
  • Readable test specifications
  • Comprehensive assertion matching

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.