or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async.mdfixtures.mdindex.mdmatchers.mdproperty.mdscalactic.mdtest-styles.md

property.mddocs/

0

# Property-Based Testing

1

2

ScalaTest provides built-in support for property-based testing through generators, table-driven tests, and property checking utilities. This enables testing with automatically generated test data and verification of properties that should hold for all inputs.

3

4

## Capabilities

5

6

### Property Checks

7

8

Core trait for property-based testing that combines generator-driven and table-driven approaches.

9

10

```scala { .api }

11

trait PropertyChecks extends TableDrivenPropertyChecks with GeneratorDrivenPropertyChecks {

12

13

/**

14

* Check a property using generated values

15

*/

16

def forAll[A](gen: Generator[A])(fun: A => Assertion): Assertion

17

def forAll[A, B](genA: Generator[A], genB: Generator[B])(fun: (A, B) => Assertion): Assertion

18

def forAll[A, B, C](genA: Generator[A], genB: Generator[B], genC: Generator[C])(fun: (A, B, C) => Assertion): Assertion

19

20

/**

21

* Check a property using table data

22

*/

23

def forAll[A](table: TableFor1[A])(fun: A => Assertion): Assertion

24

def forAll[A, B](table: TableFor2[A, B])(fun: (A, B) => Assertion): Assertion

25

def forAll[A, B, C](table: TableFor3[A, B, C])(fun: (A, B, C) => Assertion): Assertion

26

27

/**

28

* Conditional property checking

29

*/

30

def whenever(condition: Boolean)(fun: => Assertion): Assertion

31

}

32

33

object PropertyChecks extends PropertyChecks

34

```

35

36

**Usage Examples:**

37

38

```scala

39

import org.scalatest.prop.PropertyChecks

40

import org.scalatest.funsuite.AnyFunSuite

41

42

class PropertyExample extends AnyFunSuite with PropertyChecks {

43

44

test("string reverse property") {

45

forAll { (s: String) =>

46

s.reverse.reverse should equal (s)

47

}

48

}

49

50

test("addition is commutative") {

51

forAll { (a: Int, b: Int) =>

52

whenever(a > 0 && b > 0) {

53

a + b should equal (b + a)

54

}

55

}

56

}

57

58

test("list concatenation properties") {

59

forAll { (list1: List[Int], list2: List[Int]) =>

60

val combined = list1 ++ list2

61

combined.length should equal (list1.length + list2.length)

62

combined.take(list1.length) should equal (list1)

63

combined.drop(list1.length) should equal (list2)

64

}

65

}

66

}

67

```

68

69

### Generators

70

71

Core generators for creating test data with shrinking support.

72

73

```scala { .api }

74

trait Generator[T] {

75

76

/**

77

* Generate the next value with shrinking support

78

*/

79

def next(szp: SizeParam, edges: List[T], rnd: Randomizer): (RoseTree[T], Randomizer)

80

81

/**

82

* Transform generated values

83

*/

84

def map[U](f: T => U): Generator[U]

85

86

/**

87

* Flat map for composing generators

88

*/

89

def flatMap[U](f: T => Generator[U]): Generator[U]

90

91

/**

92

* Filter generated values

93

*/

94

def filter(f: T => Boolean): Generator[T]

95

96

/**

97

* Create pairs of generated values

98

*/

99

def zip[U](other: Generator[U]): Generator[(T, U)]

100

101

/**

102

* Generate samples for testing

103

*/

104

def sample: Option[T]

105

def samples(n: Int): List[T]

106

}

107

108

object Generator {

109

110

/**

111

* Create generator from a function

112

*/

113

def apply[T](f: (SizeParam, List[T], Randomizer) => (RoseTree[T], Randomizer)): Generator[T]

114

115

/**

116

* Generator that always produces the same value

117

*/

118

def const[T](value: T): Generator[T]

119

120

/**

121

* Generator that chooses randomly from provided values

122

*/

123

def oneOf[T](values: T*): Generator[T]

124

def oneOf[T](gen: Generator[T], gens: Generator[T]*): Generator[T]

125

126

/**

127

* Generator that chooses from a weighted distribution

128

*/

129

def frequency[T](weightedGens: (Int, Generator[T])*): Generator[T]

130

131

/**

132

* Generator for lists with specified size range

133

*/

134

def listOfN[T](n: Int, gen: Generator[T]): Generator[List[T]]

135

def listOf[T](gen: Generator[T]): Generator[List[T]]

136

}

137

```

138

139

**Usage Examples:**

140

141

```scala

142

import org.scalatest.prop.Generator

143

144

// Custom generators

145

val evenIntGen = Generator.choose(0, 100).map(_ * 2)

146

val nonEmptyStringGen = Generator.alphaStr.filter(_.nonEmpty)

147

148

// Composed generators

149

val personGen = for {

150

name <- Generator.alphaStr.filter(_.nonEmpty)

151

age <- Generator.choose(0, 120)

152

email <- Generator.alphaStr.map(_ + "@example.com")

153

} yield Person(name, age, email)

154

155

// Using generators in tests

156

forAll(evenIntGen) { n =>

157

n % 2 should equal (0)

158

}

159

160

forAll(personGen) { person =>

161

person.name should not be empty

162

person.age should be >= 0

163

person.email should include ("@")

164

}

165

```

166

167

### Common Generators

168

169

Pre-built generators for common data types.

170

171

```scala { .api }

172

trait CommonGenerators {

173

174

// Numeric generators

175

def choose(min: Int, max: Int): Generator[Int]

176

def choose(min: Double, max: Double): Generator[Double]

177

def chooseNum[T: Numeric](min: T, max: T): Generator[T]

178

179

// String generators

180

def alphaChar: Generator[Char]

181

def alphaNumChar: Generator[Char]

182

def alphaStr: Generator[String]

183

def alphaNumStr: Generator[String]

184

def numStr: Generator[String]

185

186

// Collection generators

187

def listOf[T](gen: Generator[T]): Generator[List[T]]

188

def vectorOf[T](gen: Generator[T]): Generator[Vector[T]]

189

def setOf[T](gen: Generator[T]): Generator[Set[T]]

190

def mapOf[K, V](keyGen: Generator[K], valueGen: Generator[V]): Generator[Map[K, V]]

191

192

// Option and Either generators

193

def option[T](gen: Generator[T]): Generator[Option[T]]

194

def either[A, B](genA: Generator[A], genB: Generator[B]): Generator[Either[A, B]]

195

196

// Tuple generators

197

def tuple2[A, B](genA: Generator[A], genB: Generator[B]): Generator[(A, B)]

198

def tuple3[A, B, C](genA: Generator[A], genB: Generator[B], genC: Generator[C]): Generator[(A, B, C)]

199

}

200

201

object CommonGenerators extends CommonGenerators

202

```

203

204

**Usage Examples:**

205

206

```scala

207

import org.scalatest.prop.CommonGenerators._

208

209

// Using pre-built generators

210

val emailGen = for {

211

username <- alphaStr.filter(_.nonEmpty)

212

domain <- alphaStr.filter(_.nonEmpty)

213

} yield s"$username@$domain.com"

214

215

val phoneGen = numStr.map(_.take(10).padTo(10, '0'))

216

217

val addressGen = for {

218

street <- alphaNumStr

219

city <- alphaStr

220

zipCode <- numStr.map(_.take(5))

221

} yield Address(street, city, zipCode)

222

```

223

224

### Table-Driven Testing

225

226

Structured approach to testing with predefined data sets.

227

228

```scala { .api }

229

/**

230

* Table with one column of test data

231

*/

232

case class TableFor1[A](heading: String, rows: A*) extends Iterable[A] {

233

def iterator: Iterator[A] = rows.iterator

234

}

235

236

/**

237

* Table with two columns of test data

238

*/

239

case class TableFor2[A, B](heading1: String, heading2: String, rows: (A, B)*) extends Iterable[(A, B)] {

240

def iterator: Iterator[(A, B)] = rows.iterator

241

}

242

243

/**

244

* Helper for creating tables

245

*/

246

object Table {

247

def apply[A](heading: String, rows: A*): TableFor1[A] = TableFor1(heading, rows: _*)

248

def apply[A, B](heading1: String, heading2: String, rows: (A, B)*): TableFor2[A, B] =

249

TableFor2(heading1, heading2, rows: _*)

250

}

251

252

trait TableDrivenPropertyChecks {

253

/**

254

* Check property for all rows in table

255

*/

256

def forAll[A](table: TableFor1[A])(fun: A => Assertion): Assertion

257

def forAll[A, B](table: TableFor2[A, B])(fun: (A, B) => Assertion): Assertion

258

}

259

```

260

261

**Usage Examples:**

262

263

```scala

264

import org.scalatest.prop.{Table, TableDrivenPropertyChecks}

265

266

class TableDrivenExample extends AnyFunSuite with TableDrivenPropertyChecks {

267

268

test("mathematical operations") {

269

val examples = Table(

270

("input", "expected"),

271

(2, 4),

272

(3, 9),

273

(4, 16),

274

(5, 25)

275

)

276

277

forAll(examples) { (input, expected) =>

278

input * input should equal (expected)

279

}

280

}

281

282

test("string operations") {

283

val stringExamples = Table(

284

"input" -> "expected",

285

"hello" -> "HELLO",

286

"world" -> "WORLD",

287

"test" -> "TEST"

288

)

289

290

forAll(stringExamples) { (input, expected) =>

291

input.toUpperCase should equal (expected)

292

}

293

}

294

}

295

```

296

297

### Configuration

298

299

Configurable parameters for property-based testing.

300

301

```scala { .api }

302

trait Configuration {

303

304

/**

305

* Minimum successful tests before property passes

306

*/

307

def minSuccessful: Int = 100

308

309

/**

310

* Maximum number of discarded tests before failure

311

*/

312

def maxDiscarded: Int = 500

313

314

/**

315

* Minimum size parameter for generators

316

*/

317

def minSize: Int = 0

318

319

/**

320

* Maximum size parameter for generators

321

*/

322

def maxSize: Int = 100

323

324

/**

325

* Number of workers for parallel testing

326

*/

327

def workers: Int = 1

328

}

329

330

/**

331

* Immutable configuration for property checks

332

*/

333

case class PropertyCheckConfiguration(

334

minSuccessful: Int = 100,

335

maxDiscarded: Int = 500,

336

minSize: Int = 0,

337

maxSize: Int = 100,

338

workers: Int = 1

339

) extends Configuration

340

```

341

342

**Usage Examples:**

343

344

```scala

345

// Custom configuration

346

implicit val config = PropertyCheckConfiguration(

347

minSuccessful = 50, // Fewer required successes

348

maxDiscarded = 1000, // Allow more discarded tests

349

maxSize = 200 // Larger test data

350

)

351

352

forAll { (list: List[Int]) =>

353

list.reverse.reverse should equal (list)

354

}

355

356

// Configuration for specific test

357

forAll(PropertyCheckConfiguration(minSuccessful = 1000)) { (n: Int) =>

358

math.abs(n) should be >= 0

359

}

360

```

361

362

## Types

363

364

```scala { .api }

365

/**

366

* Random value generator with seed

367

*/

368

case class Randomizer(seed: Long) {

369

def nextInt: (Int, Randomizer)

370

def nextLong: (Long, Randomizer)

371

def nextDouble: (Double, Randomizer)

372

def nextBoolean: (Boolean, Randomizer)

373

def choose[T](values: Vector[T]): (T, Randomizer)

374

}

375

376

/**

377

* Size parameter for generators

378

*/

379

case class SizeParam(value: Int) extends AnyVal {

380

def inc: SizeParam = SizeParam(value + 1)

381

def dec: SizeParam = SizeParam(math.max(0, value - 1))

382

}

383

384

/**

385

* Tree structure for shrinking test failures

386

*/

387

trait RoseTree[+T] {

388

def value: T

389

def shrinks: Stream[RoseTree[T]]

390

def map[U](f: T => U): RoseTree[U]

391

def flatMap[U](f: T => RoseTree[U]): RoseTree[U]

392

}

393

394

object RoseTree {

395

def apply[T](value: T, shrinks: Stream[RoseTree[T]] = Stream.empty): RoseTree[T]

396

}

397

```