ScalaTest is a comprehensive testing framework for Scala and Java applications that provides multiple testing styles, powerful assertion libraries, and seamless integration with build tools and IDEs.
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)Install with Tessl CLI
npx tessl i tessl/maven-org-scalatest--scalatest-2-12