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.
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]
}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
}
}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)
}
}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)
}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
}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
}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
}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()
}
}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)
}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)
}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)
}
}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)
}
}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)
}// 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"))// Floating point with tolerance
val result = complexCalculation()
result should equal(3.14159 +- 0.001)
// String case-insensitive matching
text.toLowerCase should equal(expected.toLowerCase)// 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)