or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

assertions.mdconfiguration.mdindex.mdproperty-testing.mdtest-aspects.mdtest-definition.mdtest-services.md

property-testing.mddocs/

0

# Property-Based Testing

1

2

Advanced property-based testing with sophisticated generators, automatic shrinking, and configurable test execution.

3

4

## Capabilities

5

6

### Check Functions

7

8

Run property-based tests with generated inputs to verify properties hold across many test cases.

9

10

```scala { .api }

11

/**

12

* Check that a property holds for generated values from a generator

13

* @param rv generator for test values

14

* @param test property to verify for each generated value

15

* @return test result indicating success or failure with shrunk counterexamples

16

*/

17

def check[R <: ZAny, A](rv: Gen[R, A])(test: A => TestResult): ZIO[R with TestConfig, Nothing, TestResult]

18

19

/**

20

* Check property with two generators

21

*/

22

def check[R <: ZAny, A, B](rv1: Gen[R, A], rv2: Gen[R, B])(

23

test: (A, B) => TestResult

24

): ZIO[R with TestConfig, Nothing, TestResult]

25

26

/**

27

* Check property with up to 8 generators (additional overloads available)

28

*/

29

def check[R <: ZAny, A, B, C](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C])(

30

test: (A, B, C) => TestResult

31

): ZIO[R with TestConfig, Nothing, TestResult]

32

33

/**

34

* Check that property holds for ALL values from a finite generator

35

* @param rv finite, deterministic generator

36

* @param test property to verify

37

* @return test result

38

*/

39

def checkAll[R <: ZAny, A](rv: Gen[R, A])(test: A => TestResult): ZIO[R with TestConfig, Nothing, TestResult]

40

41

/**

42

* Check property in parallel for better performance with finite generators

43

* @param rv generator for test values

44

* @param parallelism number of parallel executions

45

* @param test property to verify

46

*/

47

def checkAllPar[R <: ZAny, A](rv: Gen[R, A], parallelism: Int)(

48

test: A => TestResult

49

): ZIO[R with TestConfig, Nothing, TestResult]

50

```

51

52

**Usage Examples:**

53

54

```scala

55

import zio.test._

56

import zio.test.Gen._

57

58

// Basic property testing

59

test("list reverse is idempotent") {

60

check(listOf(anyInt)) { list =>

61

assertTrue(list.reverse.reverse == list)

62

}

63

}

64

65

// Multiple generators

66

test("addition is commutative") {

67

check(anyInt, anyInt) { (a, b) =>

68

assertTrue(a + b == b + a)

69

}

70

}

71

72

// Effectful property testing

73

test("database round trip") {

74

check(Gen.user) { user =>

75

for {

76

saved <- Database.save(user)

77

retrieved <- Database.findById(saved.id)

78

} yield assertTrue(retrieved.contains(user))

79

}

80

}

81

82

// Exhaustive testing with finite generator

83

test("boolean operations") {

84

checkAll(Gen.boolean, Gen.boolean) { (a, b) =>

85

assertTrue((a && b) == !((!a) || (!b))) // De Morgan's law

86

}

87

}

88

```

89

90

### Gen Trait

91

92

Core generator trait for producing random values with shrinking capability.

93

94

```scala { .api }

95

/**

96

* Generator for values of type A in environment R

97

*/

98

trait Gen[+R, +A] {

99

/**

100

* Transform generated values

101

* @param f transformation function

102

* @return generator producing transformed values

103

*/

104

def map[B](f: A => B): Gen[R, B]

105

106

/**

107

* Chain generators together

108

* @param f function producing dependent generator

109

* @return generator with chained dependencies

110

*/

111

def flatMap[R1 <: R, B](f: A => Gen[R1, B]): Gen[R1, B]

112

113

/**

114

* Filter generated values

115

* @param f predicate function

116

* @return generator producing only values satisfying predicate

117

*/

118

def filter(f: A => Boolean): Gen[R, A]

119

120

/**

121

* Generate a stream of samples with shrinking information

122

* @return stream of generated samples

123

*/

124

def sample: ZStream[R, Nothing, Sample[R, A]]

125

126

/**

127

* Combine with another generator using applicative

128

* @param that other generator

129

* @return generator producing tuples

130

*/

131

def <*>[R1 <: R, B](that: Gen[R1, B]): Gen[R1, (A, B)]

132

133

/**

134

* Generate values within specified size bounds

135

* @param min minimum size

136

* @param max maximum size

137

* @return sized generator

138

*/

139

def between(min: Int, max: Int): Gen[R with Sized, A]

140

}

141

```

142

143

### Primitive Generators

144

145

Built-in generators for basic data types.

146

147

```scala { .api }

148

// Numeric generators

149

val anyByte: Gen[Any, Byte]

150

val anyShort: Gen[Any, Short]

151

val anyInt: Gen[Any, Int]

152

val anyLong: Gen[Any, Long]

153

val anyFloat: Gen[Any, Float]

154

val anyDouble: Gen[Any, Double]

155

156

// Bounded numeric generators

157

def byte(min: Byte, max: Byte): Gen[Any, Byte]

158

def short(min: Short, max: Short): Gen[Any, Short]

159

def int(min: Int, max: Int): Gen[Any, Int]

160

def long(min: Long, max: Long): Gen[Any, Long]

161

def double(min: Double, max: Double): Gen[Any, Double]

162

163

// Character and string generators

164

val anyChar: Gen[Any, Char]

165

val anyString: Gen[Sized, String]

166

val anyASCIIString: Gen[Sized, String]

167

val alphaNumericString: Gen[Sized, String]

168

val alphaChar: Gen[Any, Char]

169

val numericChar: Gen[Any, Char]

170

171

// Boolean generator

172

val boolean: Gen[Any, Boolean]

173

174

// UUID generator

175

val anyUUID: Gen[Any, java.util.UUID]

176

```

177

178

**Usage Examples:**

179

180

```scala

181

import zio.test.Gen._

182

183

// Basic generators

184

test("numeric properties") {

185

check(anyInt) { n =>

186

assertTrue(n + 0 == n)

187

}

188

}

189

190

// Bounded generators

191

test("percentage calculations") {

192

check(double(0.0, 100.0)) { percentage =>

193

assertTrue(percentage >= 0.0 && percentage <= 100.0)

194

}

195

}

196

197

// String generators

198

test("string length properties") {

199

check(anyString) { str =>

200

assertTrue(str.length >= 0 && str.reverse.length == str.length)

201

}

202

}

203

```

204

205

### Collection Generators

206

207

Generators for collections with configurable sizes.

208

209

```scala { .api }

210

/**

211

* Generate lists of specified size

212

* @param n exact size of generated lists

213

* @param g generator for list elements

214

* @return generator producing lists of size n

215

*/

216

def listOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, List[A]]

217

218

/**

219

* Generate lists with sizes bounded by current Sized environment

220

* @param g generator for list elements

221

* @return generator producing variable-sized lists

222

*/

223

def listOf[R, A](g: Gen[R, A]): Gen[R with Sized, List[A]]

224

225

/**

226

* Generate lists with size between min and max

227

* @param min minimum list size

228

* @param max maximum list size

229

* @param g generator for list elements

230

*/

231

def listOfBounded[R, A](min: Int, max: Int)(g: Gen[R, A]): Gen[R, List[A]]

232

233

// Vector generators

234

def vectorOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, Vector[A]]

235

def vectorOf[R, A](g: Gen[R, A]): Gen[R with Sized, Vector[A]]

236

237

// Set generators (automatically deduplicates)

238

def setOfN[R, A](n: Int)(g: Gen[R, A]): Gen[R, Set[A]]

239

def setOf[R, A](g: Gen[R, A]): Gen[R with Sized, Set[A]]

240

241

// Map generators

242

def mapOfN[R, A, B](n: Int)(g: Gen[R, (A, B)]): Gen[R, Map[A, B]]

243

def mapOf[R, A, B](g: Gen[R, (A, B)]): Gen[R with Sized, Map[A, B]]

244

245

// Non-empty collections

246

def listOf1[R, A](g: Gen[R, A]): Gen[R with Sized, List[A]]

247

def setOf1[R, A](g: Gen[R, A]): Gen[R with Sized, Set[A]]

248

```

249

250

**Usage Examples:**

251

252

```scala

253

import zio.test.Gen._

254

255

// Fixed-size collections

256

test("list operations") {

257

check(listOfN(5)(anyInt)) { list =>

258

assertTrue(list.size == 5 && list.reverse.reverse == list)

259

}

260

}

261

262

// Variable-size collections

263

test("set properties") {

264

check(setOf(anyInt)) { set =>

265

assertTrue(set.union(set) == set && set.intersect(set) == set)

266

}

267

}

268

269

// Non-empty collections

270

test("head and tail operations") {

271

check(listOf1(anyString)) { list =>

272

assertTrue(list.nonEmpty && list.head :: list.tail == list)

273

}

274

}

275

276

// Maps

277

test("map properties") {

278

check(mapOf(anyString.zip(anyInt))) { map =>

279

assertTrue(map.keys.toSet.size <= map.size)

280

}

281

}

282

```

283

284

### Generator Combinators

285

286

Functions for combining and transforming generators.

287

288

```scala { .api }

289

/**

290

* Choose randomly from provided generators

291

* @param first first generator option

292

* @param rest additional generator options

293

* @return generator that randomly selects from provided options

294

*/

295

def oneOf[R, A](first: Gen[R, A], rest: Gen[R, A]*): Gen[R, A]

296

297

/**

298

* Create generator from effectful computation

299

* @param effect ZIO effect producing values

300

* @return generator that runs the effect

301

*/

302

def fromZIO[R, A](effect: ZIO[R, Nothing, A]): Gen[R, A]

303

304

/**

305

* Generate values using unfold pattern

306

* @param s initial state

307

* @param f function from state to next state and value

308

* @return generator using unfold pattern

309

*/

310

def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, A]

311

312

/**

313

* Choose from generators with specified weights

314

* @param generators weighted generator options

315

* @return generator that selects based on weights

316

*/

317

def weighted[R, A](generators: (Gen[R, A], Double)*): Gen[R, A]

318

319

/**

320

* Generate constant value

321

* @param a constant value to generate

322

* @return generator always producing the constant

323

*/

324

def const[A](a: => A): Gen[Any, A]

325

326

/**

327

* Generate from explicit list of values

328

* @param first first value option

329

* @param rest additional value options

330

* @return generator randomly selecting from provided values

331

*/

332

def elements[A](first: A, rest: A*): Gen[Any, A]

333

334

/**

335

* Generate optional values (Some or None)

336

* @param g generator for Some values

337

* @return generator producing Option[A]

338

*/

339

def option[R, A](g: Gen[R, A]): Gen[R, Option[A]]

340

341

/**

342

* Generate Either values

343

* @param left generator for Left values

344

* @param right generator for Right values

345

* @return generator producing Either[A, B]

346

*/

347

def either[R, A, B](left: Gen[R, A], right: Gen[R, B]): Gen[R, Either[A, B]]

348

349

/**

350

* Generate tuples

351

* @param g1 generator for first element

352

* @param g2 generator for second element

353

* @return generator producing tuples

354

*/

355

def zip[R, A, B](g1: Gen[R, A], g2: Gen[R, B]): Gen[R, (A, B)]

356

```

357

358

**Usage Examples:**

359

360

```scala

361

import zio.test.Gen._

362

363

// Choose from generators

364

val stringOrInt: Gen[Any, Any] = oneOf(anyString, anyInt)

365

366

// Weighted choice

367

val mostlyPositive: Gen[Any, Int] = weighted(

368

int(1, 100) -> 0.8,

369

int(-100, 0) -> 0.2

370

)

371

372

// Constant and elements

373

val httpMethods = elements("GET", "POST", "PUT", "DELETE")

374

val alwaysTrue = const(true)

375

376

// Option and Either

377

test("optional values") {

378

check(option(anyInt)) { optInt =>

379

optInt match {

380

case Some(n) => assertTrue(n.toString.nonEmpty)

381

case None => assertTrue(true)

382

}

383

}

384

}

385

386

test("either values") {

387

check(either(anyString, anyInt)) { stringOrInt =>

388

stringOrInt match {

389

case Left(s) => assertTrue(s.isInstanceOf[String])

390

case Right(n) => assertTrue(n.isInstanceOf[Int])

391

}

392

}

393

}

394

```

395

396

### Custom Generators

397

398

Building domain-specific generators for complex data types.

399

400

```scala { .api }

401

/**

402

* Create generator that may fail to produce values

403

* @param pf partial function defining generation logic

404

* @return generator with filtering logic

405

*/

406

def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, A]

407

408

/**

409

* Create recursive generators with size control

410

* @param base base case generator for small sizes

411

* @param rec recursive case for larger sizes

412

* @return size-aware recursive generator

413

*/

414

def sized[R, A](f: Int => Gen[R, A]): Gen[R with Sized, A]

415

416

/**

417

* Generate from a ZIO effect

418

* @param effect effectful value generation

419

* @return generator wrapping the effect

420

*/

421

def fromZIO[R, A](effect: ZIO[R, Nothing, A]): Gen[R, A]

422

```

423

424

**Usage Examples:**

425

426

```scala

427

import zio.test.Gen._

428

429

// Custom data types

430

case class User(id: Int, name: String, email: String, age: Int)

431

432

val genUser: Gen[Any, User] = for {

433

id <- int(1, 10000)

434

name <- elements("Alice", "Bob", "Charlie", "Diana")

435

domain <- elements("example.com", "test.org", "demo.net")

436

age <- int(18, 80)

437

} yield User(id, name, s"${name.toLowerCase}@$domain", age)

438

439

// Recursive data structures

440

sealed trait Tree[A]

441

case class Leaf[A](value: A) extends Tree[A]

442

case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

443

444

def genTree[A](genA: Gen[Any, A]): Gen[Sized, Tree[A]] = {

445

val genLeaf = genA.map(Leaf(_))

446

val genBranch = for {

447

left <- genTree(genA)

448

right <- genTree(genA)

449

} yield Branch(left, right)

450

451

sized { size =>

452

if (size <= 1) genLeaf

453

else oneOf(genLeaf, genBranch)

454

}

455

}

456

457

// Using custom generators

458

test("user validation") {

459

check(genUser) { user =>

460

assertTrue(

461

user.id > 0 &&

462

user.name.nonEmpty &&

463

user.email.contains("@") &&

464

user.age >= 18

465

)

466

}

467

}

468

```

469

470

## Sample Type

471

472

Represents generated values with shrinking information for counterexample minimization.

473

474

```scala { .api }

475

/**

476

* A generated sample with shrinking capability

477

*/

478

trait Sample[+R, +A] {

479

/**

480

* The generated value

481

*/

482

def value: A

483

484

/**

485

* Stream of shrunk variants of this sample

486

* @return stream of smaller samples for counterexample minimization

487

*/

488

def shrink: ZStream[R, Nothing, Sample[R, A]]

489

490

/**

491

* Transform the sample value

492

* @param f transformation function

493

* @return sample with transformed value

494

*/

495

def map[B](f: A => B): Sample[R, B]

496

497

/**

498

* Transform with environment modification

499

* @param f environment transformation

500

* @return sample in transformed environment

501

*/

502

def mapZIO[R1, B](f: A => ZIO[R1, Nothing, B]): Sample[R1, B]

503

}

504

```

505

506

## Sized Environment

507

508

Controls the size of generated collections and data structures.

509

510

```scala { .api }

511

/**

512

* Environment service that provides size bounds for generators

513

*/

514

case class Sized(size: Int) extends AnyVal

515

516

object Sized {

517

/**

518

* Create a Sized service with fixed size

519

* @param size the size value

520

* @return layer providing Sized service

521

*/

522

def live(size: Int): ULayer[Sized]

523

524

/**

525

* Access current size from environment

526

*/

527

val size: URIO[Sized, Int]

528

}

529

```

530

531

**Usage Examples:**

532

533

```scala

534

import zio.test._

535

536

// Control generation size

537

test("large collections").provideLayer(Sized.live(1000)) {

538

check(listOf(anyInt)) { largeList =>

539

assertTrue(largeList.size <= 1000)

540

}

541

}

542

543

// Size-aware generators

544

val genSizedString: Gen[Sized, String] = sized { size =>

545

listOfN(size)(alphaChar).map(_.mkString)

546

}

547

```