0
# Asynchronous Testing
1
2
ScalaTest provides comprehensive support for testing asynchronous code including Futures, eventual consistency patterns, time-based assertions, and concurrent test execution. The framework offers async test suite variants, future handling utilities, retry mechanisms, and timeout controls for robust asynchronous testing.
3
4
## Capabilities
5
6
### Async Test Suites
7
8
Async variants of all test styles that return `Future[Assertion]` instead of `Assertion`, enabling proper async test execution.
9
10
```scala { .api }
11
/**
12
* Base trait for asynchronous test suites
13
*/
14
trait AsyncTestSuite extends Suite {
15
/**
16
* Implicit execution context for Future operations
17
*/
18
implicit def executionContext: ExecutionContext
19
20
/**
21
* Transform assertion Future to compatible result type
22
*/
23
final def transformToOutcome(futureAssertion: Future[compatible.Assertion]): AsyncOutcome
24
}
25
26
/**
27
* Async function-based test suite
28
*/
29
abstract class AsyncFunSuite extends AsyncTestSuite with TestSuite {
30
/**
31
* Register async test returning Future[Assertion]
32
* @param testName the name of the test
33
* @param testFun async test function returning Future[Assertion]
34
*/
35
protected def test(testName: String)(testFun: => Future[compatible.Assertion]): Unit
36
37
protected def ignore(testName: String)(testFun: => Future[compatible.Assertion]): Unit
38
}
39
40
// Similar async variants for all test styles:
41
abstract class AsyncFlatSpec extends AsyncTestSuite with TestSuite
42
abstract class AsyncWordSpec extends AsyncTestSuite with TestSuite
43
abstract class AsyncFreeSpec extends AsyncTestSuite with TestSuite
44
abstract class AsyncFunSpec extends AsyncTestSuite with TestSuite
45
abstract class AsyncFeatureSpec extends AsyncTestSuite with TestSuite
46
abstract class AsyncPropSpec extends AsyncTestSuite with TestSuite
47
```
48
49
**Usage Example:**
50
51
```scala
52
import org.scalatest.funsuite.AsyncFunSuite
53
import org.scalatest.matchers.should.Matchers
54
import scala.concurrent.Future
55
import scala.concurrent.duration._
56
57
class AsyncServiceSpec extends AsyncFunSuite with Matchers {
58
59
test("async operation should complete successfully") {
60
val service = new AsyncUserService()
61
62
// Return Future[Assertion] - ScalaTest handles async completion
63
service.createUser("John", "john@example.com").map { user =>
64
user.name should equal("John")
65
user.email should equal("john@example.com")
66
user.id should not be empty
67
}
68
}
69
70
test("async operation should handle errors") {
71
val service = new AsyncUserService()
72
73
// Test async failures
74
recoverToSucceededIf[ValidationException] {
75
service.createUser("", "invalid-email")
76
}
77
}
78
79
test("multiple async operations") {
80
val service = new AsyncUserService()
81
82
for {
83
user1 <- service.createUser("Alice", "alice@example.com")
84
user2 <- service.createUser("Bob", "bob@example.com")
85
users <- service.getUsers()
86
} yield {
87
users should contain(user1)
88
users should contain(user2)
89
users should have size 2
90
}
91
}
92
}
93
```
94
95
### Future Testing with ScalaFutures
96
97
Test Scala Futures with patience configuration and automatic waiting.
98
99
```scala { .api }
100
/**
101
* Utilities for testing Scala Futures
102
*/
103
trait ScalaFutures extends Futures with PatienceConfiguration {
104
/**
105
* Wait for future completion and apply assertions
106
* @param future the Future to wait for
107
* @param fun assertions to apply to the completed value
108
*/
109
def whenReady[T](future: Future[T])(fun: T => Unit): Unit
110
111
/**
112
* Wait for future completion with custom patience config
113
*/
114
def whenReady[T](future: Future[T], config: PatienceConfig)(fun: T => Unit): Unit
115
116
/**
117
* Implicit conversion to enable .futureValue syntax
118
*/
119
implicit def convertScalaFuture[T](future: Future[T]): FutureValue[T]
120
}
121
122
/**
123
* Enhanced Future with testing methods
124
*/
125
final class FutureValue[T](future: Future[T]) {
126
/**
127
* Block and return the future's value (with timeout)
128
*/
129
def futureValue: T
130
131
/**
132
* Block and return value with custom patience
133
*/
134
def futureValue(config: PatienceConfig): T
135
}
136
```
137
138
**Usage Examples:**
139
140
```scala
141
import org.scalatest.funsuite.AnyFunSuite
142
import org.scalatest.matchers.should.Matchers
143
import org.scalatest.concurrent.ScalaFutures
144
import org.scalatest.time.{Seconds, Span}
145
import scala.concurrent.Future
146
147
class FutureTestingSpec extends AnyFunSuite with Matchers with ScalaFutures {
148
149
test("testing futures with whenReady") {
150
val future = Future {
151
Thread.sleep(100)
152
"Hello, World!"
153
}
154
155
whenReady(future) { result =>
156
result should equal("Hello, World!")
157
result should startWith("Hello")
158
}
159
}
160
161
test("testing futures with futureValue") {
162
val future = asyncComputation()
163
164
// Block until completion and get value
165
future.futureValue should be > 0
166
future.futureValue should be <= 100
167
}
168
169
test("custom patience configuration") {
170
val slowFuture = Future {
171
Thread.sleep(2000)
172
42
173
}
174
175
// Custom timeout for slow operations
176
whenReady(slowFuture, timeout(Span(5, Seconds))) { result =>
177
result should equal(42)
178
}
179
}
180
181
test("testing future failures") {
182
val failingFuture = Future {
183
throw new RuntimeException("Something went wrong")
184
}
185
186
// Test that future fails with expected exception
187
whenReady(failingFuture.failed) { exception =>
188
exception shouldBe a[RuntimeException]
189
exception.getMessage should include("went wrong")
190
}
191
}
192
}
193
```
194
195
### Eventual Consistency with Eventually
196
197
Test systems that achieve consistency over time using retry mechanisms.
198
199
```scala { .api }
200
/**
201
* Retry assertions until they succeed or timeout
202
*/
203
trait Eventually extends PatienceConfiguration {
204
/**
205
* Retry assertion until success or timeout
206
* @param fun assertion block to retry
207
* @return successful assertion result
208
*/
209
def eventually[T](fun: => T): T
210
211
/**
212
* Eventually with custom patience configuration
213
*/
214
def eventually[T](config: PatienceConfig)(fun: => T): T
215
}
216
```
217
218
**Usage Examples:**
219
220
```scala
221
import org.scalatest.funsuite.AnyFunSuite
222
import org.scalatest.matchers.should.Matchers
223
import org.scalatest.concurrent.Eventually
224
import org.scalatest.time.{Seconds, Millis, Span}
225
226
class EventualConsistencySpec extends AnyFunSuite with Matchers with Eventually {
227
228
test("eventually consistent cache") {
229
val cache = new EventuallyConsistentCache()
230
cache.put("key", "value")
231
232
// May not be immediately available, but should be eventually
233
eventually {
234
cache.get("key") should equal(Some("value"))
235
}
236
}
237
238
test("distributed system synchronization") {
239
val cluster = new DistributedCluster()
240
cluster.addNode("node1", "data")
241
242
// Data should replicate to all nodes eventually
243
eventually(timeout(Span(10, Seconds)), interval(Span(500, Millis))) {
244
cluster.getAllNodes().foreach { node =>
245
node.getData() should contain("data")
246
}
247
}
248
}
249
250
test("UI state changes") {
251
val ui = new AsyncUI()
252
ui.startLoading()
253
254
// UI should show loading state eventually
255
eventually {
256
ui.isLoading should be(true)
257
ui.getLoadingText() should equal("Loading...")
258
}
259
260
ui.finishLoading()
261
262
// UI should stop loading eventually
263
eventually {
264
ui.isLoading should be(false)
265
}
266
}
267
}
268
```
269
270
### Time Limits and Timeouts
271
272
Impose time constraints on test execution and operations.
273
274
```scala { .api }
275
/**
276
* Impose time limits on test operations
277
*/
278
trait TimeLimits {
279
/**
280
* Fail test if operation takes longer than specified timeout
281
* @param timeout maximum allowed time
282
* @param fun operation to time-limit
283
* @return result if completed within timeout
284
*/
285
def failAfter[T](timeout: Span)(fun: => T): T
286
287
/**
288
* Cancel test if operation takes longer than timeout
289
*/
290
def cancelAfter[T](timeout: Span)(fun: => T): T
291
}
292
293
/**
294
* Automatic time limits for all tests in suite
295
*/
296
trait TimeLimitedTests extends TimeLimits {
297
/**
298
* Default timeout applied to all tests
299
*/
300
def timeLimit: Span
301
302
/**
303
* Override for specific tests that need different timeouts
304
*/
305
override def withFixture(test: NoArgTest): Outcome = {
306
failAfter(timeLimit)(super.withFixture(test))
307
}
308
}
309
310
/**
311
* Async version of time-limited tests
312
*/
313
trait AsyncTimeLimitedTests extends AsyncTestSuite with TimeLimits {
314
def timeLimit: Span
315
316
override def withFixture(test: NoArgAsyncTest): FutureOutcome = {
317
val superWithFixture = super.withFixture(test)
318
val timedFuture = failAfter(timeLimit)(superWithFixture.toFuture)
319
new FutureOutcome(timedFuture)
320
}
321
}
322
```
323
324
**Usage Examples:**
325
326
```scala
327
import org.scalatest.funsuite.AnyFunSuite
328
import org.scalatest.matchers.should.Matchers
329
import org.scalatest.concurrent.{TimeLimits, TimeLimitedTests}
330
import org.scalatest.time.{Seconds, Span}
331
332
class TimeoutSpec extends AnyFunSuite with Matchers with TimeLimits {
333
334
test("operation should complete within time limit") {
335
failAfter(Span(2, Seconds)) {
336
val result = performExpensiveCalculation()
337
result should be > 0
338
}
339
}
340
341
test("slow operation should timeout") {
342
intercept[TestFailedDueToTimeoutException] {
343
failAfter(Span(1, Seconds)) {
344
Thread.sleep(2000) // Takes longer than 1 second
345
"completed"
346
}
347
}
348
}
349
}
350
351
class AutoTimeLimitedSpec extends AnyFunSuite with Matchers with TimeLimitedTests {
352
353
// All tests in this suite automatically fail after 5 seconds
354
def timeLimit = Span(5, Seconds)
355
356
test("fast test completes normally") {
357
val result = quickOperation()
358
result should not be null
359
}
360
361
test("slow test gets automatic timeout") {
362
// This test will automatically fail after 5 seconds
363
Thread.sleep(6000) // Takes longer than timeLimit
364
}
365
}
366
```
367
368
### Patience Configuration
369
370
Configure timeout and retry behavior for async operations.
371
372
```scala { .api }
373
/**
374
* Configuration for operations that require waiting
375
*/
376
trait PatienceConfiguration {
377
/**
378
* Configuration for timeouts and retry intervals
379
*/
380
case class PatienceConfig(
381
timeout: Span, // Maximum time to wait
382
interval: Span // Time between retry attempts
383
)
384
385
/**
386
* Default patience configuration
387
*/
388
implicit def patienceConfig: PatienceConfig
389
390
/**
391
* Convenience methods for creating timeouts
392
*/
393
def timeout(value: Span): PatienceConfig
394
def interval(value: Span): PatienceConfig
395
}
396
397
/**
398
* Extended timeouts for integration testing
399
*/
400
trait IntegrationPatience extends PatienceConfiguration {
401
// Longer default timeouts suitable for integration tests
402
implicit override val patienceConfig: PatienceConfig =
403
PatienceConfig(timeout = Span(15, Seconds), interval = Span(150, Millis))
404
}
405
```
406
407
**Usage Examples:**
408
409
```scala
410
import org.scalatest.time.{Seconds, Millis, Span}
411
import org.scalatest.concurrent.{Eventually, IntegrationPatience}
412
413
class PatienceConfigSpec extends AnyFunSuite with Matchers
414
with Eventually with IntegrationPatience {
415
416
test("custom patience for specific operation") {
417
val service = new SlowExternalService()
418
419
// Custom patience for this specific test
420
eventually(timeout(Span(30, Seconds)), interval(Span(1, Seconds))) {
421
service.isHealthy() should be(true)
422
}
423
}
424
425
test("integration test with extended patience") {
426
// Uses IntegrationPatience defaults (15 seconds timeout)
427
val database = new DatabaseConnection()
428
429
eventually {
430
database.isConnected() should be(true)
431
database.getStatus() should equal("ready")
432
}
433
}
434
}
435
```
436
437
### Concurrent Test Coordination
438
439
Coordinate multiple threads and test concurrent behavior.
440
441
```scala { .api }
442
/**
443
* Coordinate multi-threaded test scenarios
444
*/
445
trait Conductors {
446
/**
447
* Create a conductor for orchestrating concurrent test execution
448
*/
449
def conductor: Conductor
450
}
451
452
/**
453
* Conductor for multi-threaded test coordination
454
*/
455
class Conductor {
456
/**
457
* Execute function in a separate thread
458
* @param fun function to execute concurrently
459
*/
460
def thread[T](fun: => T): Thread
461
462
/**
463
* Wait for all threads to complete
464
*/
465
def whenFinished(fun: => Unit): Unit
466
}
467
468
/**
469
* Thread synchronization utilities
470
*/
471
trait Waiters {
472
/**
473
* Create a waiter for thread coordination
474
*/
475
def waiter(): Waiter
476
}
477
478
class Waiter {
479
/**
480
* Signal that an expected event occurred
481
*/
482
def dismiss(): Unit
483
484
/**
485
* Wait for expected events with timeout
486
*/
487
def await(timeout: Span = Span(150, Millis)): Unit
488
}
489
```
490
491
**Usage Examples:**
492
493
```scala
494
import org.scalatest.funsuite.AnyFunSuite
495
import org.scalatest.matchers.should.Matchers
496
import org.scalatest.concurrent.{Conductors, Waiters}
497
import java.util.concurrent.atomic.AtomicInteger
498
499
class ConcurrentTestSpec extends AnyFunSuite with Matchers
500
with Conductors with Waiters {
501
502
test("concurrent counter increments") {
503
val counter = new AtomicInteger(0)
504
val conductor = this.conductor
505
506
conductor.thread {
507
for (i <- 1 to 100) {
508
counter.incrementAndGet()
509
}
510
}
511
512
conductor.thread {
513
for (i <- 1 to 100) {
514
counter.incrementAndGet()
515
}
516
}
517
518
conductor.whenFinished {
519
counter.get() should equal(200)
520
}
521
}
522
523
test("producer-consumer coordination") {
524
val buffer = new java.util.concurrent.ArrayBlockingQueue[String](10)
525
val waiter = this.waiter()
526
527
val producer = new Thread {
528
override def run(): Unit = {
529
buffer.put("item1")
530
buffer.put("item2")
531
waiter.dismiss() // Signal items are available
532
}
533
}
534
535
val consumer = new Thread {
536
override def run(): Unit = {
537
waiter.await() // Wait for items
538
val item1 = buffer.take()
539
val item2 = buffer.take()
540
item1 should equal("item1")
541
item2 should equal("item2")
542
}
543
}
544
545
producer.start()
546
consumer.start()
547
548
producer.join()
549
consumer.join()
550
}
551
}
552
```
553
554
## Common Async Testing Patterns
555
556
### Testing Async Services
557
558
```scala
559
test("async service integration") {
560
val service = new AsyncEmailService()
561
562
for {
563
result <- service.sendEmail("user@example.com", "Hello", "Test message")
564
status <- service.getDeliveryStatus(result.messageId)
565
} yield {
566
result.success should be(true)
567
status should equal("delivered")
568
}
569
}
570
```
571
572
### Error Handling in Async Tests
573
574
```scala
575
test("async error handling") {
576
val service = new AsyncUserService()
577
578
// Test successful recovery
579
recoverToSucceededIf[ValidationException] {
580
service.createUser("", "invalid-email")
581
}
582
583
// Test specific error details
584
service.createUser("", "").failed.map { exception =>
585
exception shouldBe a[ValidationException]
586
exception.getMessage should include("name")
587
}
588
}
589
```
590
591
### Testing Event Streams
592
593
```scala
594
test("event stream processing") {
595
val eventStream = new AsyncEventStream()
596
eventStream.start()
597
598
eventually {
599
eventStream.getProcessedCount() should be > 0
600
}
601
602
eventStream.stop()
603
604
eventually {
605
eventStream.isRunning() should be(false)
606
}
607
}
608
```
609
610
### Parallel Test Execution
611
612
```scala
613
// Enable parallel execution for the entire suite
614
class ParallelTestSuite extends AnyFunSuite with ParallelTestExecution {
615
// Tests in this suite run in parallel by default
616
617
test("independent test 1") {
618
// This can run concurrently with other tests
619
performIndependentOperation()
620
}
621
622
test("independent test 2") {
623
// This can also run concurrently
624
performAnotherIndependentOperation()
625
}
626
}
627
```