or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced.mdassertions.mdasync.mdfixtures.mdindex.mdmatchers.mdtest-styles.md
tile.json

matchers.mddocs/

Matchers

ScalaTest's matcher framework provides an expressive DSL for writing readable and maintainable test assertions. The framework offers two main DSL styles - "should" and "must" - with extensive built-in matchers covering equality, numeric comparisons, collections, strings, types, exceptions, and more.

Capabilities

Core Matcher Framework

The foundation of the matcher system with base traits and result types.

/**
 * Base trait for all matchers
 */
trait Matcher[-T] {
  /**
   * Apply the matcher to a value
   * @param left the value to match against
   * @return MatchResult indicating success/failure with messages
   */
  def apply(left: T): MatchResult
  
  /**
   * Compose this matcher with a transformation function
   * @param f function to transform input before matching
   * @return new matcher that applies transformation first
   */
  def compose[U](f: U => T): Matcher[U]
}

/**
 * Result of applying a matcher
 */
case class MatchResult(
  matches: Boolean,                        // Whether the match succeeded
  failureMessage: String,                  // Message displayed on failure
  negatedFailureMessage: String,           // Message displayed when negated match fails
  midSentenceFailureMessage: String = "",  // Message for mid-sentence contexts
  midSentenceNegatedFailureMessage: String = ""  // Negated mid-sentence message
)

/**
 * Matcher for use with "be" syntax
 */
trait BeMatcher[-T] {
  def apply(left: T): MatchResult
}

/**
 * Matcher for object properties with "be" syntax
 */
trait BePropertyMatcher[-T] {
  def apply(objectWithProperty: T): BePropertyMatchResult
}

/**
 * Matcher for object properties with "have" syntax  
 */
trait HavePropertyMatcher[-T, +P] {
  def apply(objectWithProperty: T): HavePropertyMatchResult[P]
}

Should Matchers DSL

The primary matcher DSL using "should" syntax for natural, readable assertions.

/**
 * Complete "should" matcher DSL - mix into test suites for matcher functionality
 */
trait Matchers extends ShouldVerb with MatcherWords with Tolerance {
  // Enables: value should matcher
  // All matcher methods and implicit conversions are available
}

/**
 * Core "should" verb that enables matcher syntax
 */
trait ShouldVerb {
  implicit def convertToAnyShouldWrapper[T](o: T): AnyShouldWrapper[T]
  
  final class AnyShouldWrapper[T](val leftSideValue: T) {
    def should(rightMatcherX1: Matcher[T]): Assertion
    def should(notWord: NotWord): ResultOfNotWordForAny[T]
    def shouldNot(rightMatcherX1: Matcher[T]): Assertion
  }
}

Equality Matchers

Test value equality with various comparison strategies.

/**
 * Test exact equality
 * @param right expected value
 */
def equal[T](right: T): Matcher[T]

/**
 * Symbolic equality matcher (same as equal)
 */
def ===[T](right: T): Matcher[T] 

/**
 * Reference equality matcher
 */
def be[T <: AnyRef](right: T): Matcher[T]

/**
 * "be" with tolerance for floating point comparison
 */
def be(right: Double): Matcher[Double]
def be(right: Float): Matcher[Float]

Usage Examples:

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

class EqualityMatcherSpec extends AnyFunSuite with Matchers {
  
  test("equality matchers") {
    val x = 42
    val y = 42
    val list1 = List(1, 2, 3)
    val list2 = List(1, 2, 3)
    
    // Value equality
    x should equal(42)
    x should ===(y)
    list1 should equal(list2)
    
    // Reference equality (for AnyRef)
    val str1 = new String("hello")
    val str2 = new String("hello") 
    str1 should equal(str2)      // true - value equality
    str1 should not be str2      // true - different references
    
    // Negation
    x should not equal 43
    x shouldNot equal(43)
  }
  
  test("floating point equality with tolerance") {
    val result = 0.1 + 0.2
    
    // Direct equality often fails due to floating point precision
    // result should equal(0.3)  // might fail
    
    // Use tolerance for floating point comparison
    result should equal(0.3 +- 0.001)
    result should be(0.3 +- 0.001)
  }
}

Numeric Matchers

Compare numeric values with relational operators.

/**
 * Numeric comparison matchers
 */
def be > [T](right: T)(implicit ord: Ordering[T]): Matcher[T]
def be >= [T](right: T)(implicit ord: Ordering[T]): Matcher[T]  
def be < [T](right: T)(implicit ord: Ordering[T]): Matcher[T]
def be <= [T](right: T)(implicit ord: Ordering[T]): Matcher[T]

/**
 * Tolerance matcher for floating point comparison
 */
def +-(tolerance: Double): Spread[Double]
def +-(tolerance: Float): Spread[Float]

Usage Examples:

test("numeric matchers") {
  val score = 85
  val price = 29.99
  val count = 0
  
  // Relational comparisons
  score should be > 80
  score should be >= 85
  score should be < 100
  score should be <= 85
  
  // Works with any ordered type
  price should be > 20.0
  count should be >= 0
  
  // Tolerance comparison
  val calculation = 10.0 / 3.0
  calculation should equal(3.333 +- 0.01)
  calculation should be(3.333 +- 0.001)
}

String Matchers

Specialized matchers for string content and patterns.

/**
 * String content matchers
 */
def startWith(right: String): Matcher[String]
def endWith(right: String): Matcher[String]  
def include(right: String): Matcher[String]

/**
 * Regular expression matchers
 */
def fullyMatch(right: Regex): Matcher[String]
def include regex(right: Regex): Matcher[String]
def startWith regex(right: Regex): Matcher[String]
def endWith regex(right: Regex): Matcher[String]

/**
 * Length matcher for strings and collections
 */
def have length(expectedLength: Long): HavePropertyMatcher[AnyRef, Long]
def have size(expectedSize: Long): HavePropertyMatcher[AnyRef, Long]

Usage Examples:

test("string matchers") {
  val message = "Hello, World!"
  val email = "user@example.com"
  val code = "ABC123"
  
  // Content matching
  message should startWith("Hello")
  message should endWith("World!")
  message should include("lo, Wo")
  
  // Case sensitivity
  message should startWith("hello") // fails
  message should startWith("Hello") // succeeds
  
  // Email validation with regex
  val emailPattern = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$""".r
  email should fullyMatch(emailPattern)
  
  // Partial regex matching
  code should include regex "[0-9]+".r
  code should startWith regex "[A-Z]+".r
  
  // Length checking
  message should have length 13
  email should have size 16
}

Collection Matchers

Comprehensive matchers for collections, sequences, and iterables.

/**
 * Collection content matchers
 */
def contain[T](right: T): Matcher[GenTraversable[T]]
def contain oneOf[T](firstEle: T, secondEle: T, remainingEles: T*): Matcher[GenTraversable[T]]
def contain allOf[T](firstEle: T, secondEle: T, remainingEles: T*): Matcher[GenTraversable[T]]
def contain noneOf[T](firstEle: T, secondEle: T, remainingEles: T*): Matcher[GenTraversable[T]]
def contain only[T](right: T*): Matcher[GenTraversable[T]]
def contain inOrderOnly[T](firstEle: T, secondEle: T, remainingEles: T*): Matcher[GenTraversable[T]]
def contain theSameElementsAs[T](right: GenTraversable[T]): Matcher[GenTraversable[T]]
def contain theSameElementsInOrderAs[T](right: GenTraversable[T]): Matcher[GenTraversable[T]]

/**
 * Collection property matchers
 */
def be(empty: EmptyWord): Matcher[AnyRef with java.util.Collection[_]]
def have size[T](expectedSize: Long): HavePropertyMatcher[scala.collection.GenTraversable[T], Long]
def have length[T](expectedLength: Long): HavePropertyMatcher[AnyRef, Long]

/**
 * Sequence-specific matchers
 */
def be(sorted: SortedWord): Matcher[scala.collection.GenSeq[T]]

Usage Examples:

test("collection matchers") {
  val numbers = List(1, 2, 3, 4, 5)
  val names = Set("Alice", "Bob", "Charlie")
  val empty = List.empty[String]
  val duplicates = List(1, 2, 2, 3)
  
  // Element membership
  numbers should contain(3)
  numbers should contain oneOf(3, 6, 9)
  numbers should contain allOf(1, 3, 5)
  numbers should contain noneOf(6, 7, 8)
  
  // Exact content matching
  numbers should contain only(1, 2, 3, 4, 5)
  numbers should contain theSameElementsAs(List(5, 4, 3, 2, 1))
  
  // Order-sensitive matching  
  numbers should contain inOrderOnly(1, 2, 3, 4, 5)
  numbers should contain theSameElementsInOrderAs(List(1, 2, 3, 4, 5))
  
  // Collection properties
  empty should be(empty)
  numbers should have size 5
  numbers should have length 5
  numbers should be(sorted)
  duplicates shouldNot be(sorted)
  
  // Set operations
  names should contain("Alice")
  names should have size 3
}

Type and Class Matchers

Test object types, class membership, and inheritance relationships.

/**
 * Type testing matchers
 */
def a[T: ClassTag]: AMatcher[Any]
def an[T: ClassTag]: AnMatcher[Any]

/**
 * Class testing matchers  
 */
def be(aType: ResultOfATypeInvocation[_]): Matcher[Any]
def be(anType: ResultOfAnTypeInvocation[_]): Matcher[Any]

Usage Examples:

test("type and class matchers") {
  val obj: Any = "hello"
  val num: Any = 42
  val list: Any = List(1, 2, 3)
  
  // Type checking with articles
  obj should be a 'string  // deprecated syntax
  obj should be a String
  obj should be an instanceOf[String]
  
  num should be an Integer 
  num should be an instanceOf[Integer]
  
  list should be a List[_]
  list should be an instanceOf[List[_]]
  
  // Class checking
  obj shouldNot be an Integer
  num shouldNot be a String
}

Exception Matchers

Test that code throws expected exceptions with optional message checking.

/**
 * Exception testing matchers
 */
def thrownBy(codeBlock: => Any): ResultOfThrownByInvocation
def a[T <: AnyRef]: ResultOfATypeInvocation[T] 
def an[T <: AnyRef]: ResultOfAnTypeInvocation[T]

// Usage: an[ExceptionType] should be thrownBy { code }

Usage Examples:

test("exception matchers") {
  // Test that specific exception type is thrown
  an[IllegalArgumentException] should be thrownBy {
    require(false, "This should fail")
  }
  
  a[RuntimeException] should be thrownBy {
    throw new RuntimeException("Something went wrong")
  }
  
  // Test exception message content
  val exception = the[ValidationException] thrownBy {
    validateUser(invalidUser)
  }
  exception.getMessage should include("email")
  exception.getFieldName should equal("email")
  
  // Test that no exception is thrown
  noException should be thrownBy {
    safeOperation()
  }
}

Boolean and Property Matchers

Test boolean values and object properties.

/**
 * Boolean value matchers
 */
def be(true: TrueWord): Matcher[Boolean]
def be(false: FalseWord): Matcher[Boolean]

/**
 * Property existence matchers
 */
def be(defined: DefinedWord): Matcher[Option[_]]
def be(empty: EmptyWord): Matcher[AnyRef]
def be(readable: ReadableWord): Matcher[java.io.File]
def be(writable: WritableWord): Matcher[java.io.File]
def be(sorted: SortedWord): Matcher[scala.collection.GenSeq[_]]

Usage Examples:

test("boolean and property matchers") {
  val isValid = true
  val result: Option[String] = Some("value")
  val emptyResult: Option[String] = None
  val numbers = List(1, 2, 3, 4)
  val scrambled = List(3, 1, 4, 2)
  
  // Boolean testing
  isValid should be(true)
  !isValid should be(false)
  
  // Option testing
  result should be(defined)
  emptyResult shouldNot be(defined)
  
  // Collection properties
  List.empty should be(empty)
  numbers shouldNot be(empty)
  numbers should be(sorted)
  scrambled shouldNot be(sorted)
}

File and IO Matchers

Test file system properties and IO operations.

/**
 * File property matchers
 */
def exist: Matcher[java.io.File]
def be(readable: ReadableWord): Matcher[java.io.File]
def be(writable: WritableWord): Matcher[java.io.File]  
def be(executable: ExecutableWord): Matcher[java.io.File]

Usage Examples:

import java.io.File

test("file matchers") {
  val configFile = new File("config.properties")
  val tempFile = new File("/tmp/test.txt")
  val scriptFile = new File("deploy.sh")
  
  // File existence
  configFile should exist
  new File("nonexistent.txt") shouldNot exist
  
  // File permissions (platform dependent)
  configFile should be(readable)
  tempFile should be(writable)
  scriptFile should be(executable)
}

Must Matchers DSL

Alternative DSL using "must" instead of "should" - identical functionality with different syntax.

/**
 * "Must" matcher DSL - alternative to "should" syntax
 */
trait MustMatchers extends MustVerb with MatcherWords with Tolerance {
  // Enables: value must matcher
  // All same matchers available as "should" DSL
}

implicit def convertToAnyMustWrapper[T](o: T): AnyMustWrapper[T]

final class AnyMustWrapper[T](val leftSideValue: T) {
  def must(rightMatcherX1: Matcher[T]): Assertion
  def must(notWord: NotWord): ResultOfNotWordForAny[T]  
  def mustNot(rightMatcherX1: Matcher[T]): Assertion
}

Usage Example:

import org.scalatest.funsuite.AnyFunSuite  
import org.scalatest.matchers.must.Matchers

class MustMatcherSpec extends AnyFunSuite with Matchers {
  
  test("must syntax examples") {
    val value = 42
    val text = "Hello World"
    val items = List(1, 2, 3)
    
    // Same matchers, different syntax
    value must equal(42)
    value must be > 40
    value mustNot equal(43)
    
    text must startWith("Hello")
    text must include("World")
    text must have length 11
    
    items must contain(2)
    items must have size 3
    items must be(sorted)
  }
}

Custom Matchers

Create domain-specific matchers for reusable, expressive test assertions.

/**
 * Base for creating custom matchers
 */
trait Matcher[-T] {
  def apply(left: T): MatchResult
}

/**
 * Helper for creating simple matchers
 */
def Matcher[T](fun: T => MatchResult): Matcher[T]

Usage Examples:

import org.scalatest.matchers.{MatchResult, Matcher}

class CustomMatcherSpec extends AnyFunSuite with Matchers {
  
  // Custom matcher for even numbers
  def beEven: Matcher[Int] = Matcher { (left: Int) =>
    MatchResult(
      left % 2 == 0,
      s"$left was not even",
      s"$left was even"
    )
  }
  
  // Custom matcher for valid email addresses
  def beValidEmail: Matcher[String] = Matcher { (left: String) =>
    val isValid = left.contains("@") && left.contains(".") && 
                  !left.startsWith("@") && !left.endsWith("@")
    MatchResult(
      isValid,
      s"'$left' was not a valid email address",
      s"'$left' was a valid email address"
    )
  }
  
  // Custom matcher with parameters
  def haveWordsCount(expectedCount: Int): Matcher[String] = Matcher { (left: String) =>
    val actualCount = left.split("\\s+").length
    MatchResult(
      actualCount == expectedCount,
      s"'$left' had $actualCount words instead of $expectedCount",
      s"'$left' had $expectedCount words"
    )
  }
  
  test("custom matchers in action") {
    // Using custom matchers
    4 should beEven
    3 shouldNot beEven
    
    "user@example.com" should beValidEmail
    "invalid-email" shouldNot beValidEmail
    
    "Hello beautiful world" should haveWordsCount(3)
    "Single" should haveWordsCount(1)
  }
}

Matcher Composition

Combine and transform matchers for complex assertions.

Usage Examples:

test("matcher composition") {
  val users = List(
    User("Alice", 25, "alice@example.com"),
    User("Bob", 30, "bob@example.com"), 
    User("Charlie", 35, "charlie@example.com")
  )
  
  // Compose matchers with transformations
  users should contain(beValidEmail compose (_.email))
  users should contain(be > 25 compose (_.age))
  
  // Multiple composition
  val activeAdults = beTrue compose ((u: User) => u.age >= 18 && u.isActive)
  users should contain(activeAdults)
}

Common Patterns

Combining Multiple Matchers

// Multiple assertions on the same value
val user = getUser(123)
user.name should (startWith("John") and endWith("Doe"))
user.age should (be >= 18 and be <= 100)
user.email should (include("@") and endWith(".com"))

Tolerant Matching

// Floating point with tolerance
val result = complexCalculation()
result should equal(3.14159 +- 0.001)

// String case-insensitive matching  
text.toLowerCase should equal(expected.toLowerCase)

Collection Testing Patterns

// Testing collection transformations
numbers.map(_ * 2) should contain theSameElementsAs List(2, 4, 6, 8)

// Testing filtering
users.filter(_.isActive) should have size 3
users.filter(_.age > 25) should contain only (alice, bob)