or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

core-specifications.mddsl-components.mdindex.mdintegration-features.mdmatcher-system.mdmutable-specifications.mdreporting.mdtest-execution.md

matcher-system.mddocs/

0

# Matcher System

1

2

The specs2 matcher system provides a comprehensive set of type-safe matchers for assertions in specifications. Matchers can be composed, negated, and customized to create expressive and readable test assertions.

3

4

## Core Matcher Types

5

6

### Matcher[T]

7

8

Base trait for all matchers providing composition and transformation methods.

9

10

```scala { .api }

11

trait Matcher[T] {

12

def apply[S <: T](expectable: Expectable[S]): MatchResult[S]

13

14

// Composition methods

15

def and[S <: T](m: Matcher[S]): Matcher[S]

16

def or[S <: T](m: Matcher[S]): Matcher[S]

17

def not: Matcher[T]

18

19

// Transformation methods

20

def ^^[S](f: S => T): Matcher[S]

21

def when(condition: => Boolean): Matcher[T]

22

def unless(condition: => Boolean): Matcher[T]

23

def iff(condition: => Boolean): Matcher[T]

24

25

// Utility methods

26

def eventually: Matcher[T]

27

def lazily: Matcher[T]

28

def orSkip: Matcher[T]

29

def orPending: Matcher[T]

30

}

31

```

32

33

### MatchResult[T]

34

35

Result of applying a matcher to a value.

36

37

```scala { .api }

38

case class MatchResult[T](

39

expectable: Expectable[T],

40

message: Message,

41

negatedMessage: Message

42

) {

43

def isSuccess: Boolean

44

def isFailure: Boolean

45

def message: String

46

def negatedMessage: String

47

}

48

```

49

50

### Expectable[T]

51

52

Wrapper for values being tested with additional metadata.

53

54

```scala { .api }

55

case class Expectable[T](

56

value: T,

57

description: String = "",

58

showValue: Boolean = true

59

) {

60

def must[R](m: Matcher[R]): MatchResult[T]

61

def should[R](m: Matcher[R]): MatchResult[T]

62

def aka(alias: String): Expectable[T]

63

def updateDescription(f: String => String): Expectable[T]

64

}

65

```

66

67

## Matcher Collections

68

69

### Matchers

70

71

Main trait aggregating all standard matchers.

72

73

```scala { .api }

74

trait Matchers extends AnyMatchers

75

with BeHaveMatchers

76

with TraversableMatchers

77

with MapMatchers

78

with StringMatchers

79

with NumericMatchers

80

with ExceptionMatchers

81

with OptionMatchers

82

with EitherMatchers

83

with FutureMatchers

84

with EventuallyMatchers

85

with XmlMatchers

86

with JsonMatchers

87

```

88

89

### MustMatchers

90

91

"Must" syntax for expectations that return results.

92

93

```scala { .api }

94

trait MustMatchers extends Matchers {

95

implicit def anyToMust[T](t: T): MustExpectable[T]

96

}

97

98

class MustExpectable[T](value: T) {

99

def must[R](m: Matcher[R]): MatchResult[T]

100

def must(m: Matcher[T]): MatchResult[T]

101

}

102

```

103

104

### ShouldMatchers

105

106

"Should" syntax for expectations.

107

108

```scala { .api }

109

trait ShouldMatchers extends Matchers {

110

implicit def anyToShould[T](t: T): ShouldExpectable[T]

111

}

112

113

class ShouldExpectable[T](value: T) {

114

def should[R](m: Matcher[R]): MatchResult[T]

115

def should(m: Matcher[T]): MatchResult[T]

116

}

117

```

118

119

## Core Matcher Categories

120

121

### AnyMatchers

122

123

General-purpose matchers for any type.

124

125

```scala { .api }

126

trait AnyMatchers {

127

def beTrue: Matcher[Boolean]

128

def beFalse: Matcher[Boolean]

129

def beNull[T]: Matcher[T]

130

def beEqualTo[T](t: T): Matcher[T]

131

def be_===[T](t: T): Matcher[T]

132

def beOneOf[T](values: T*): Matcher[T]

133

def beAnInstanceOf[T: ClassTag]: Matcher[Any]

134

def haveClass[T: ClassTag]: Matcher[Any]

135

def haveSuperclass[T: ClassTag]: Matcher[Any]

136

def beAssignableFrom[T: ClassTag]: Matcher[Class[_]]

137

}

138

```

139

140

**Usage Examples:**

141

```scala

142

true must beTrue

143

null must beNull

144

5 must beEqualTo(5)

145

"test" must beOneOf("test", "spec", "example")

146

List(1,2,3) must beAnInstanceOf[List[Int]]

147

```

148

149

### StringMatchers

150

151

String-specific matchers.

152

153

```scala { .api }

154

trait StringMatchers {

155

def contain(s: String): Matcher[String]

156

def startWith(s: String): Matcher[String]

157

def endWith(s: String): Matcher[String]

158

def beMatching(regex: String): Matcher[String]

159

def beMatching(regex: Regex): Matcher[String]

160

def find(regex: String): Matcher[String]

161

def have size(n: Int): Matcher[String]

162

def have length(n: Int): Matcher[String]

163

def be empty: Matcher[String]

164

def beBlank: Matcher[String]

165

}

166

```

167

168

**Usage Examples:**

169

```scala

170

"hello world" must contain("world")

171

"specs2" must startWith("spec")

172

"testing" must endWith("ing")

173

"abc123" must beMatching("\\w+\\d+")

174

"" must be empty

175

" " must beBlank

176

```

177

178

### TraversableMatchers

179

180

Matchers for collections and traversable types.

181

182

```scala { .api }

183

trait TraversableMatchers {

184

def contain[T](t: T): Matcher[Traversable[T]]

185

def containMatch[T](regex: String): Matcher[Traversable[T]]

186

def containPattern[T](regex: String): Matcher[Traversable[T]]

187

def haveSize[T](n: Int): Matcher[Traversable[T]]

188

def haveLength[T](n: Int): Matcher[Traversable[T]]

189

def be empty[T]: Matcher[Traversable[T]]

190

def beSorted[T: Ordering]: Matcher[Traversable[T]]

191

def containAllOf[T](elements: T*): Matcher[Traversable[T]]

192

def containAnyOf[T](elements: T*): Matcher[Traversable[T]]

193

def atLeastOnce[T](m: Matcher[T]): Matcher[Traversable[T]]

194

def atMostOnce[T](m: Matcher[T]): Matcher[Traversable[T]]

195

def exactly[T](n: Int, m: Matcher[T]): Matcher[Traversable[T]]

196

}

197

```

198

199

**Usage Examples:**

200

```scala

201

List(1, 2, 3) must contain(2)

202

List(1, 2, 3) must haveSize(3)

203

List.empty[Int] must be empty

204

List(1, 2, 3) must beSorted

205

List(1, 2, 3, 2) must containAllOf(1, 2)

206

List("a", "b", "c") must atLeastOnce(startWith("a"))

207

```

208

209

### NumericMatchers

210

211

Numeric comparison matchers.

212

213

```scala { .api }

214

trait NumericMatchers {

215

def beLessThan[T: Ordering](n: T): Matcher[T]

216

def beLessThanOrEqualTo[T: Ordering](n: T): Matcher[T]

217

def beGreaterThan[T: Ordering](n: T): Matcher[T]

218

def beGreaterThanOrEqualTo[T: Ordering](n: T): Matcher[T]

219

def beBetween[T: Ordering](min: T, max: T): Matcher[T]

220

def beCloseTo[T: Numeric](expected: T, delta: T): Matcher[T]

221

def bePositive[T: Numeric]: Matcher[T]

222

def beNegative[T: Numeric]: Matcher[T]

223

def beZero[T: Numeric]: Matcher[T]

224

}

225

```

226

227

**Usage Examples:**

228

```scala

229

5 must beLessThan(10)

230

10 must beGreaterThanOrEqualTo(10)

231

7 must beBetween(5, 10)

232

3.14159 must beCloseTo(3.14, 0.01)

233

5 must bePositive

234

-3 must beNegative

235

0 must beZero

236

```

237

238

### ExceptionMatchers

239

240

Matchers for testing exceptions.

241

242

```scala { .api }

243

trait ExceptionMatchers {

244

def throwA[E <: Throwable: ClassTag]: Matcher[Any]

245

def throwAn[E <: Throwable: ClassTag]: Matcher[Any]

246

def throwA[E <: Throwable: ClassTag](message: String): Matcher[Any]

247

def throwA[E <: Throwable: ClassTag](messagePattern: Regex): Matcher[Any]

248

def throwA[E <: Throwable: ClassTag](matcher: Matcher[E]): Matcher[Any]

249

}

250

```

251

252

**Usage Examples:**

253

```scala

254

{ throw new IllegalArgumentException("bad arg") } must throwAn[IllegalArgumentException]

255

{ 1 / 0 } must throwA[ArithmeticException]

256

{ validate("") } must throwA[ValidationException]("empty input")

257

{ parse("invalid") } must throwA[ParseException](beMatching(".*invalid.*"))

258

```

259

260

### OptionMatchers

261

262

Matchers for Option types.

263

264

```scala { .api }

265

trait OptionMatchers {

266

def beSome[T]: Matcher[Option[T]]

267

def beSome[T](t: T): Matcher[Option[T]]

268

def beSome[T](matcher: Matcher[T]): Matcher[Option[T]]

269

def beNone[T]: Matcher[Option[T]]

270

}

271

```

272

273

**Usage Examples:**

274

```scala

275

Some(5) must beSome

276

Some("test") must beSome("test")

277

Some(10) must beSome(beGreaterThan(5))

278

None must beNone

279

```

280

281

### EitherMatchers

282

283

Matchers for Either types.

284

285

```scala { .api }

286

trait EitherMatchers {

287

def beRight[T]: Matcher[Either[_, T]]

288

def beRight[T](t: T): Matcher[Either[_, T]]

289

def beRight[T](matcher: Matcher[T]): Matcher[Either[_, T]]

290

def beLeft[T]: Matcher[Either[T, _]]

291

def beLeft[T](t: T): Matcher[Either[T, _]]

292

def beLeft[T](matcher: Matcher[T]): Matcher[Either[T, _]]

293

}

294

```

295

296

**Usage Examples:**

297

```scala

298

Right(42) must beRight

299

Right("success") must beRight("success")

300

Left("error") must beLeft

301

Left(404) must beLeft(beGreaterThan(400))

302

```

303

304

### FutureMatchers

305

306

Matchers for asynchronous Future types.

307

308

```scala { .api }

309

trait FutureMatchers {

310

def await[T]: Matcher[Future[T]]

311

def await[T](duration: Duration): Matcher[Future[T]]

312

def beEqualTo[T](t: T): FutureMatcher[T]

313

def throwA[E <: Throwable: ClassTag]: FutureMatcher[Any]

314

}

315

316

trait FutureMatcher[T] extends Matcher[Future[T]] {

317

def await: Matcher[Future[T]]

318

def await(duration: Duration): Matcher[Future[T]]

319

}

320

```

321

322

**Usage Examples:**

323

```scala

324

Future(42) must beEqualTo(42).await

325

Future.failed(new RuntimeException) must throwA[RuntimeException].await

326

Future(slow()) must beEqualTo(result).await(5.seconds)

327

```

328

329

### EventuallyMatchers

330

331

Retry-based matchers for eventually consistent conditions.

332

333

```scala { .api }

334

trait EventuallyMatchers {

335

def eventually[T](m: Matcher[T]): Matcher[T]

336

def eventually[T](m: Matcher[T], retries: Int): Matcher[T]

337

def eventually[T](m: Matcher[T], sleep: Duration): Matcher[T]

338

def eventually[T](m: Matcher[T], retries: Int, sleep: Duration): Matcher[T]

339

def retry[T](m: Matcher[T]): Matcher[T]

340

def atMost[T](duration: Duration): RetryMatcher[T]

341

def atLeast[T](duration: Duration): RetryMatcher[T]

342

}

343

```

344

345

**Usage Examples:**

346

```scala

347

asyncOperation() must eventually(beEqualTo(expected))

348

database.count() must eventually(beGreaterThan(0), retries = 10)

349

cache.get(key) must eventually(beSome, 100.millis)

350

```

351

352

### MapMatchers

353

354

Matchers for Map types.

355

356

```scala { .api }

357

trait MapMatchers {

358

def haveKey[K](k: K): Matcher[Map[K, _]]

359

def haveValue[V](v: V): Matcher[Map[_, V]]

360

def havePair[K, V](k: K, v: V): Matcher[Map[K, V]]

361

def havePairs[K, V](pairs: (K, V)*): Matcher[Map[K, V]]

362

def haveKeys[K](keys: K*): Matcher[Map[K, _]]

363

def haveValues[V](values: V*): Matcher[Map[_, V]]

364

}

365

```

366

367

**Usage Examples:**

368

```scala

369

Map("a" -> 1, "b" -> 2) must haveKey("a")

370

Map("a" -> 1, "b" -> 2) must haveValue(1)

371

Map("a" -> 1, "b" -> 2) must havePair("a" -> 1)

372

Map("a" -> 1, "b" -> 2) must haveKeys("a", "b")

373

```

374

375

## Matcher Composition

376

377

### Logical Composition

378

379

Combine matchers with logical operators:

380

381

```scala

382

// AND composition

383

result must (beGreaterThan(0) and beLessThan(100))

384

385

// OR composition

386

result must (beEqualTo("success") or beEqualTo("ok"))

387

388

// Negation

389

result must not(beEmpty)

390

result must not(contain("error"))

391

```

392

393

### Conditional Matching

394

395

Apply matchers conditionally:

396

397

```scala

398

// When condition is true

399

result must beEqualTo(expected).when(enableValidation)

400

401

// Unless condition is true

402

result must beEmpty.unless(hasData)

403

404

// If and only if condition is true

405

result must bePositive.iff(isEnabled)

406

```

407

408

### Transformation

409

410

Transform values before matching:

411

412

```scala

413

// Transform with function

414

users must haveSize(3) ^^ (_.filter(_.active))

415

416

// Transform with partial function

417

response must beEqualTo(200) ^^ { case HttpResponse(code, _) => code }

418

```

419

420

## Custom Matchers

421

422

### Creating Custom Matchers

423

424

```scala { .api }

425

def customMatcher[T](f: T => Boolean, description: String): Matcher[T] = {

426

(t: T) => {

427

val result = f(t)

428

MatchResult(result, s"$t $description", s"$t does not $description")

429

}

430

}

431

```

432

433

**Examples:**

434

```scala

435

def beValidEmail = beMatching("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b".r) ^^

436

((_: String).toLowerCase, "a valid email address")

437

438

def haveValidChecksum = customMatcher[File](

439

file => calculateChecksum(file) == expectedChecksum,

440

"have valid checksum"

441

)

442

443

"user@example.com" must beValidEmail

444

new File("data.txt") must haveValidChecksum

445

```

446

447

### Matcher Combinators

448

449

Build complex matchers from simple ones:

450

451

```scala

452

def beValidUser = (

453

have(name = not(beEmpty)) and

454

have(email = beValidEmail) and

455

have(age = beGreaterThan(0))

456

)

457

458

User("john", "john@test.com", 25) must beValidUser

459

```

460

461

## Advanced Features

462

463

### BeHaveMatchers

464

465

Natural language matchers using "be" and "have":

466

467

```scala { .api }

468

trait BeHaveMatchers {

469

def be(m: Matcher[Any]): Matcher[Any]

470

def have(m: Matcher[Any]): Matcher[Any]

471

}

472

```

473

474

**Usage:**

475

```scala

476

user must be(valid)

477

list must have(size(3))

478

file must be(readable)

479

response must have(status(200))

480

```

481

482

### Scope Matchers

483

484

Test values within specific scopes:

485

486

```scala

487

users must contain { user: User =>

488

user.name must startWith("John")

489

user.age must beGreaterThan(18)

490

}

491

```

492

493

### Message Customization

494

495

Customize matcher failure messages:

496

497

```scala

498

def bePositive = be_>=(0) ^^ ((_: Int), "a positive number")

499

500

(-5) must bePositive

501

// Failure: -5 is not a positive number

502

```

503

504

## Best Practices

505

506

1. **Use descriptive matchers**: Choose matchers that clearly express intent

507

2. **Compose logically**: Use `and`/`or` for complex conditions

508

3. **Custom matchers for domain logic**: Create domain-specific matchers for better readability

509

4. **Handle async properly**: Use `eventually` and `await` for asynchronous operations

510

5. **Meaningful failure messages**: Customize messages for better debugging

511

6. **Type safety**: Leverage Scala's type system for compile-time safety

512

7. **Performance considerations**: Be mindful of expensive operations in matcher composition