0
# Fixtures and Lifecycle Management
1
2
ScalaTest provides multiple patterns for managing test setup, teardown, and shared resources across test executions. The framework offers simple before/after hooks, parameterized fixtures, loan patterns, and sophisticated lifecycle management to ensure clean, isolated, and efficient test execution.
3
4
## Capabilities
5
6
### Basic Lifecycle Hooks
7
8
Simple setup and teardown hooks that execute around each test or entire test suite.
9
10
```scala { .api }
11
/**
12
* Simple before/after hooks for each test
13
*/
14
trait BeforeAndAfter {
15
/**
16
* Execute before each test method
17
*/
18
protected def before(): Unit
19
20
/**
21
* Execute after each test method (even if test fails)
22
*/
23
protected def after(): Unit
24
}
25
26
/**
27
* Enhanced per-test lifecycle hooks
28
*/
29
trait BeforeAndAfterEach {
30
/**
31
* Execute before each test with access to test data
32
*/
33
protected def beforeEach(): Unit
34
35
/**
36
* Execute after each test with access to test data
37
*/
38
protected def afterEach(): Unit
39
}
40
41
/**
42
* Per-test hooks with test metadata access
43
*/
44
trait BeforeAndAfterEachTestData {
45
/**
46
* Execute before each test with test information
47
* @param testData metadata about the test being executed
48
*/
49
protected def beforeEach(testData: TestData): Unit
50
51
/**
52
* Execute after each test with test information
53
* @param testData metadata about the test that was executed
54
*/
55
protected def afterEach(testData: TestData): Unit
56
}
57
```
58
59
**Usage Examples:**
60
61
```scala
62
import org.scalatest.funsuite.AnyFunSuite
63
import org.scalatest.matchers.should.Matchers
64
import org.scalatest.{BeforeAndAfter, BeforeAndAfterEach}
65
66
class DatabaseTestSpec extends AnyFunSuite with Matchers with BeforeAndAfter {
67
68
var database: TestDatabase = _
69
70
before {
71
// Setup before each test
72
database = new TestDatabase()
73
database.connect()
74
database.createTestTables()
75
}
76
77
after {
78
// Cleanup after each test
79
database.dropTestTables()
80
database.disconnect()
81
}
82
83
test("user creation should work") {
84
val user = User("Alice", "alice@example.com")
85
database.save(user)
86
87
val retrieved = database.findByEmail("alice@example.com")
88
retrieved should be(defined)
89
retrieved.get.name should equal("Alice")
90
}
91
92
test("user deletion should work") {
93
val user = User("Bob", "bob@example.com")
94
database.save(user)
95
database.delete(user.id)
96
97
database.findByEmail("bob@example.com") should be(empty)
98
}
99
}
100
101
class LoggingTestSpec extends AnyFunSuite with BeforeAndAfterEach {
102
103
override def beforeEach(): Unit = {
104
println(s"Starting test: ${getClass.getSimpleName}")
105
TestLogger.setLevel(LogLevel.DEBUG)
106
}
107
108
override def afterEach(): Unit = {
109
TestLogger.clearLogs()
110
println(s"Finished test: ${getClass.getSimpleName}")
111
}
112
113
test("service should log operations") {
114
val service = new UserService()
115
service.createUser("Test User")
116
117
TestLogger.getMessages() should contain("Creating user: Test User")
118
}
119
}
120
```
121
122
### Suite-Level Lifecycle Hooks
123
124
Setup and teardown that execute once per test suite rather than per test.
125
126
```scala { .api }
127
/**
128
* Suite-level lifecycle hooks
129
*/
130
trait BeforeAndAfterAll {
131
/**
132
* Execute once before all tests in the suite
133
*/
134
protected def beforeAll(): Unit
135
136
/**
137
* Execute once after all tests in the suite (even if tests fail)
138
*/
139
protected def afterAll(): Unit
140
}
141
142
/**
143
* Suite-level hooks with configuration access
144
*/
145
trait BeforeAndAfterAllConfigMap {
146
/**
147
* Execute before all tests with access to configuration
148
* @param configMap configuration passed to the test run
149
*/
150
protected def beforeAll(configMap: ConfigMap): Unit
151
152
/**
153
* Execute after all tests with access to configuration
154
* @param configMap configuration passed to the test run
155
*/
156
protected def afterAll(configMap: ConfigMap): Unit
157
}
158
```
159
160
**Usage Examples:**
161
162
```scala
163
import org.scalatest.funsuite.AnyFunSuite
164
import org.scalatest.matchers.should.Matchers
165
import org.scalatest.BeforeAndAfterAll
166
167
class IntegrationTestSpec extends AnyFunSuite with Matchers with BeforeAndAfterAll {
168
169
var server: TestServer = _
170
var database: TestDatabase = _
171
172
override def beforeAll(): Unit = {
173
// Expensive setup once per suite
174
database = new TestDatabase()
175
database.migrate()
176
177
server = new TestServer(database)
178
server.start()
179
180
println("Test environment ready")
181
}
182
183
override def afterAll(): Unit = {
184
// Cleanup once per suite
185
try {
186
server.stop()
187
database.cleanup()
188
} catch {
189
case e: Exception => println(s"Cleanup error: ${e.getMessage}")
190
}
191
}
192
193
test("API should create users") {
194
val response = server.post("/users", """{"name": "Alice"}""")
195
response.status should equal(201)
196
response.body should include("Alice")
197
}
198
199
test("API should list users") {
200
server.post("/users", """{"name": "Bob"}""")
201
val response = server.get("/users")
202
203
response.status should equal(200)
204
response.body should include("Bob")
205
}
206
}
207
```
208
209
### Parameterized Fixtures
210
211
Pass specific fixture objects to each test method using the fixture traits.
212
213
```scala { .api }
214
/**
215
* Base for fixture-based test suites
216
*/
217
trait fixture.TestSuite extends Suite {
218
/**
219
* The type of fixture object passed to tests
220
*/
221
type FixtureParam
222
223
/**
224
* Create and potentially cleanup fixture for each test
225
* @param test the test function that receives the fixture
226
*/
227
def withFixture(test: OneArgTest): Outcome
228
}
229
230
/**
231
* Fixture variants for all test styles
232
*/
233
abstract class fixture.FunSuite extends fixture.TestSuite {
234
/**
235
* Register test that receives fixture parameter
236
* @param testName name of the test
237
* @param testFun test function receiving fixture
238
*/
239
protected def test(testName: String)(testFun: FixtureParam => Any): Unit
240
}
241
242
// Similar fixture variants available:
243
// fixture.FlatSpec, fixture.WordSpec, fixture.FreeSpec,
244
// fixture.FunSpec, fixture.FeatureSpec, fixture.PropSpec
245
```
246
247
**Usage Examples:**
248
249
```scala
250
import org.scalatest.fixture
251
import org.scalatest.matchers.should.Matchers
252
import org.scalatest.Outcome
253
254
class FixtureExampleSpec extends fixture.FunSuite with Matchers {
255
256
// Define the fixture type
257
type FixtureParam = TestDatabase
258
259
// Create fixture for each test
260
def withFixture(test: OneArgTest): Outcome = {
261
val database = new TestDatabase()
262
database.connect()
263
database.createTestTables()
264
265
try {
266
test(database) // Pass fixture to test
267
} finally {
268
database.dropTestTables()
269
database.disconnect()
270
}
271
}
272
273
test("user operations with database fixture") { db =>
274
val user = User("Alice", "alice@example.com")
275
db.save(user)
276
277
val retrieved = db.findByEmail("alice@example.com")
278
retrieved should be(defined)
279
retrieved.get.name should equal("Alice")
280
}
281
282
test("concurrent user access") { db =>
283
val user1 = User("Bob", "bob@example.com")
284
val user2 = User("Charlie", "charlie@example.com")
285
286
db.save(user1)
287
db.save(user2)
288
289
db.count() should equal(2)
290
}
291
}
292
```
293
294
### Fixture Composition and Stackable Traits
295
296
Combine multiple fixture patterns using stackable traits.
297
298
```scala { .api }
299
/**
300
* Stack multiple fixture traits together
301
*/
302
trait fixture.TestSuiteMixin extends fixture.TestSuite {
303
// Stackable fixture behavior
304
abstract override def withFixture(test: OneArgTest): Outcome
305
}
306
```
307
308
**Usage Examples:**
309
310
```scala
311
import org.scalatest.fixture
312
import org.scalatest.matchers.should.Matchers
313
import org.scalatest.{Outcome, BeforeAndAfterAll}
314
315
// Stackable database fixture
316
trait DatabaseFixture extends fixture.TestSuiteMixin {
317
this: fixture.TestSuite =>
318
319
type FixtureParam = TestDatabase
320
321
abstract override def withFixture(test: OneArgTest): Outcome = {
322
val database = new TestDatabase()
323
database.connect()
324
325
try {
326
super.withFixture(test.toNoArgTest(database))
327
} finally {
328
database.disconnect()
329
}
330
}
331
}
332
333
// Stackable HTTP client fixture
334
trait HttpClientFixture extends fixture.TestSuiteMixin {
335
this: fixture.TestSuite =>
336
337
abstract override def withFixture(test: OneArgTest): Outcome = {
338
val httpClient = new TestHttpClient()
339
httpClient.configure()
340
341
try {
342
super.withFixture(test)
343
} finally {
344
httpClient.cleanup()
345
}
346
}
347
}
348
349
class ComposedFixtureSpec extends fixture.FunSuite
350
with Matchers with BeforeAndAfterAll
351
with DatabaseFixture with HttpClientFixture {
352
353
test("integration test with multiple fixtures") { db =>
354
// Both database and HTTP client are available
355
val user = User("Integration", "integration@test.com")
356
db.save(user)
357
358
// HTTP client configured by HttpClientFixture
359
val response = httpClient.get(s"/users/${user.id}")
360
response.status should equal(200)
361
}
362
}
363
```
364
365
### Loan Pattern Fixtures
366
367
Use the loan pattern for automatic resource management with try-finally semantics.
368
369
**Usage Examples:**
370
371
```scala
372
import org.scalatest.funsuite.AnyFunSuite
373
import org.scalatest.matchers.should.Matchers
374
375
class LoanPatternSpec extends AnyFunSuite with Matchers {
376
377
// Loan pattern helper
378
def withTempFile[T](content: String)(testCode: java.io.File => T): T = {
379
val tempFile = java.io.File.createTempFile("test", ".txt")
380
java.nio.file.Files.write(tempFile.toPath, content.getBytes)
381
382
try {
383
testCode(tempFile) // Loan the resource
384
} finally {
385
tempFile.delete() // Always cleanup
386
}
387
}
388
389
def withDatabase[T](testCode: TestDatabase => T): T = {
390
val db = new TestDatabase()
391
db.connect()
392
db.createTestTables()
393
394
try {
395
testCode(db)
396
} finally {
397
db.dropTestTables()
398
db.disconnect()
399
}
400
}
401
402
test("file processing with loan pattern") {
403
withTempFile("Hello, World!") { file =>
404
val content = scala.io.Source.fromFile(file).mkString
405
content should equal("Hello, World!")
406
file.exists() should be(true)
407
}
408
// File automatically deleted after test
409
}
410
411
test("database operations with loan pattern") {
412
withDatabase { db =>
413
val user = User("Loan", "loan@example.com")
414
db.save(user)
415
416
db.findAll() should have size 1
417
db.findByEmail("loan@example.com") should be(defined)
418
}
419
// Database automatically cleaned up
420
}
421
422
test("nested loan patterns") {
423
withDatabase { db =>
424
withTempFile("config data") { configFile =>
425
// Both resources available within nested scope
426
val config = loadConfig(configFile)
427
db.updateConfig(config)
428
429
db.getConfig() should equal(config)
430
}
431
// File cleaned up, database still available
432
db.getConfig() should not be null
433
}
434
// Database cleaned up
435
}
436
}
437
```
438
439
### One Instance Per Test
440
441
Create a new test class instance for each test method, ensuring complete isolation.
442
443
```scala { .api }
444
/**
445
* Create new instance of test class for each test method
446
*/
447
trait OneInstancePerTest extends Suite {
448
// Each test gets a fresh instance of the test class
449
// Useful for mutable fixture state
450
}
451
```
452
453
**Usage Examples:**
454
455
```scala
456
import org.scalatest.funsuite.AnyFunSuite
457
import org.scalatest.matchers.should.Matchers
458
import org.scalatest.OneInstancePerTest
459
460
class IsolatedStateSpec extends AnyFunSuite with Matchers with OneInstancePerTest {
461
462
// Mutable state - each test gets a fresh instance
463
var counter = 0
464
val cache = scala.collection.mutable.Map[String, String]()
465
466
test("first test modifies state") {
467
counter = 42
468
cache("key1") = "value1"
469
470
counter should equal(42)
471
cache should have size 1
472
}
473
474
test("second test sees fresh state") {
475
// Fresh instance - counter is 0, cache is empty
476
counter should equal(0)
477
cache should be(empty)
478
479
counter = 100
480
cache("key2") = "value2"
481
}
482
483
test("third test also sees fresh state") {
484
// Another fresh instance
485
counter should equal(0)
486
cache should be(empty)
487
}
488
}
489
```
490
491
### Test Data and Configuration
492
493
Access test metadata and configuration in lifecycle hooks.
494
495
```scala { .api }
496
/**
497
* Test metadata available in lifecycle hooks
498
*/
499
case class TestData(
500
name: String, // Test name
501
configMap: ConfigMap, // Configuration passed to test run
502
tags: Set[String] // Tags applied to the test
503
)
504
505
/**
506
* Configuration map for test runs
507
*/
508
type ConfigMap = Map[String, Any]
509
```
510
511
**Usage Examples:**
512
513
```scala
514
import org.scalatest.funsuite.AnyFunSuite
515
import org.scalatest.matchers.should.Matchers
516
import org.scalatest.{BeforeAndAfterEachTestData, TestData, Tag}
517
518
object SlowTest extends Tag("slow")
519
object DatabaseTest extends Tag("database")
520
521
class TestDataExampleSpec extends AnyFunSuite with Matchers with BeforeAndAfterEachTestData {
522
523
override def beforeEach(testData: TestData): Unit = {
524
println(s"Starting test: ${testData.name}")
525
526
if (testData.tags.contains(SlowTest.name)) {
527
println("This is a slow test - setting longer timeout")
528
}
529
530
if (testData.tags.contains(DatabaseTest.name)) {
531
println("This test needs database - ensuring connection")
532
}
533
534
// Access configuration
535
val environment = testData.configMap.getOrElse("env", "test")
536
println(s"Running in environment: $environment")
537
}
538
539
override def afterEach(testData: TestData): Unit = {
540
println(s"Finished test: ${testData.name}")
541
542
if (testData.tags.contains(DatabaseTest.name)) {
543
println("Cleaning up database resources")
544
}
545
}
546
547
test("fast unit test") {
548
// Regular test
549
1 + 1 should equal(2)
550
}
551
552
test("slow integration test", SlowTest) {
553
// Tagged as slow test
554
Thread.sleep(100)
555
"slow operation" should not be empty
556
}
557
558
test("database test", DatabaseTest) {
559
// Tagged as database test
560
"database operation" should not be empty
561
}
562
563
test("complex test", SlowTest, DatabaseTest) {
564
// Multiple tags
565
"complex operation" should not be empty
566
}
567
}
568
```
569
570
### Async Fixture Support
571
572
Fixtures for asynchronous test suites that return `Future[Assertion]`.
573
574
```scala { .api }
575
/**
576
* Async fixture support
577
*/
578
abstract class fixture.AsyncFunSuite extends fixture.AsyncTestSuite {
579
protected def test(testName: String)(testFun: FixtureParam => Future[compatible.Assertion]): Unit
580
}
581
582
// Available for all async test styles:
583
// fixture.AsyncFlatSpec, fixture.AsyncWordSpec, etc.
584
```
585
586
**Usage Examples:**
587
588
```scala
589
import org.scalatest.fixture
590
import org.scalatest.matchers.should.Matchers
591
import scala.concurrent.{Future, ExecutionContext}
592
593
class AsyncFixtureSpec extends fixture.AsyncFunSuite with Matchers {
594
595
type FixtureParam = AsyncTestService
596
597
def withFixture(test: OneArgAsyncTest): FutureOutcome = {
598
val service = new AsyncTestService()
599
600
// Setup
601
val setupFuture = service.initialize()
602
603
// Run test with cleanup
604
val testFuture = setupFuture.flatMap { _ =>
605
val testResult = test(service)
606
testResult.toFuture.andThen {
607
case _ => service.cleanup() // Cleanup regardless of result
608
}
609
}
610
611
new FutureOutcome(testFuture)
612
}
613
614
test("async service operation") { service =>
615
for {
616
result <- service.processData("test data")
617
status <- service.getStatus()
618
} yield {
619
result should not be empty
620
status should equal("processing_complete")
621
}
622
}
623
}
624
```
625
626
## Common Patterns
627
628
### Database Testing Pattern
629
630
```scala
631
trait DatabaseTestSupport extends BeforeAndAfterEach {
632
this: Suite =>
633
634
var db: TestDatabase = _
635
636
override def beforeEach(): Unit = {
637
super.beforeEach()
638
db = TestDatabase.create()
639
db.runMigrations()
640
}
641
642
override def afterEach(): Unit = {
643
try {
644
db.cleanup()
645
} finally {
646
super.afterEach()
647
}
648
}
649
}
650
```
651
652
### External Service Testing
653
654
```scala
655
trait ExternalServiceFixture extends BeforeAndAfterAll {
656
this: Suite =>
657
658
var mockServer: MockWebServer = _
659
660
override def beforeAll(): Unit = {
661
super.beforeAll()
662
mockServer = new MockWebServer()
663
mockServer.start()
664
}
665
666
override def afterAll(): Unit = {
667
try {
668
mockServer.shutdown()
669
} finally {
670
super.afterAll()
671
}
672
}
673
}
674
```
675
676
### Configuration-Based Fixtures
677
678
```scala
679
class ConfigurableTestSpec extends AnyFunSuite with BeforeAndAfterAllConfigMap {
680
681
var testConfig: TestConfig = _
682
683
override def beforeAll(configMap: ConfigMap): Unit = {
684
val environment = configMap.getOrElse("env", "test").toString
685
val dbUrl = configMap.getOrElse("db.url", "jdbc:h2:mem:test").toString
686
687
testConfig = TestConfig(environment, dbUrl)
688
}
689
690
test("configuration-driven test") {
691
testConfig.environment should not be empty
692
testConfig.databaseUrl should startWith("jdbc:")
693
}
694
}
695
```