0
# Asynchronous Testing
1
2
ScalaTest provides comprehensive support for testing asynchronous code with Futures, custom execution contexts, and async test suites. This enables testing of non-blocking operations while maintaining proper test isolation and timing control.
3
4
## Capabilities
5
6
### AsyncTestSuite
7
8
Base trait for test suites that work with asynchronous operations returning Futures.
9
10
```scala { .api }
11
/**
12
* Base trait for asynchronous test suites
13
*/
14
trait AsyncTestSuite extends Suite with RecoverMethods with CompleteLastly {
15
16
/**
17
* Implicit execution context for running async operations
18
*/
19
implicit def executionContext: ExecutionContext
20
21
/**
22
* Transform test function to handle async operations
23
*/
24
def transformToFuture(testFun: => Future[compatible.Assertion]): FutureOutcome
25
26
/**
27
* Run a single asynchronous test
28
*/
29
def runAsyncTest(
30
testName: String,
31
args: Args,
32
includeIcon: Boolean,
33
invokeWithAsyncTestDataFunction: AsyncTestDataInvoker
34
): FutureOutcome
35
}
36
37
/**
38
* Async version of FunSuite
39
*/
40
abstract class AsyncFunSuite extends AsyncTestSuite with AsyncTestRegistration {
41
/**
42
* Register an asynchronous test
43
*/
44
protected def test(testName: String)(testFun: => Future[compatible.Assertion]): Unit
45
}
46
47
/**
48
* Async version of FlatSpec
49
*/
50
abstract class AsyncFlatSpec extends AsyncTestSuite with AsyncTestRegistration {
51
protected final class AsyncFlatSpecStringWrapper(string: String) {
52
def should(testFun: => Future[compatible.Assertion]): Unit
53
def must(testFun: => Future[compatible.Assertion]): Unit
54
def can(testFun: => Future[compatible.Assertion]): Unit
55
}
56
57
protected implicit def convertToAsyncFlatSpecStringWrapper(o: String): AsyncFlatSpecStringWrapper
58
}
59
60
/**
61
* Async versions of other test styles
62
*/
63
abstract class AsyncFunSpec extends AsyncTestSuite with AsyncTestRegistration
64
abstract class AsyncWordSpec extends AsyncTestSuite with AsyncTestRegistration
65
abstract class AsyncFreeSpec extends AsyncTestSuite with AsyncTestRegistration
66
abstract class AsyncFeatureSpec extends AsyncTestSuite with AsyncTestRegistration
67
```
68
69
**Usage Examples:**
70
71
```scala
72
import org.scalatest.funsuite.AsyncFunSuite
73
import scala.concurrent.Future
74
75
class AsyncExampleSuite extends AsyncFunSuite {
76
77
test("async computation should complete successfully") {
78
val futureResult = Future {
79
Thread.sleep(100) // Simulate async work
80
42
81
}
82
83
futureResult.map { result =>
84
assert(result == 42)
85
}
86
}
87
88
test("async operation with assertions") {
89
def asyncOperation(): Future[String] = Future.successful("Hello, World!")
90
91
asyncOperation().map { result =>
92
result should include ("World")
93
result should have length 13
94
}
95
}
96
}
97
```
98
99
### FutureOutcome
100
101
Wrapper for Future[Outcome] that provides transformation and composition methods.
102
103
```scala { .api }
104
/**
105
* Wrapper for Future[Outcome] with convenient transformation methods
106
*/
107
class FutureOutcome(private[scalatest] val underlying: Future[Outcome]) {
108
109
/**
110
* Transform the outcome using a function
111
*/
112
def map(f: Outcome => Outcome)(implicit executionContext: ExecutionContext): FutureOutcome
113
114
/**
115
* FlatMap operation for chaining FutureOutcomes
116
*/
117
def flatMap(f: Outcome => FutureOutcome)(implicit executionContext: ExecutionContext): FutureOutcome
118
119
/**
120
* Transform the Future[Outcome] directly
121
*/
122
def transform(f: Try[Outcome] => Try[Outcome])(implicit executionContext: ExecutionContext): FutureOutcome
123
124
/**
125
* Handle failures and recover with a different outcome
126
*/
127
def recover(pf: PartialFunction[Throwable, Outcome])(implicit executionContext: ExecutionContext): FutureOutcome
128
129
/**
130
* Convert to a Future[Outcome]
131
*/
132
def toFuture: Future[Outcome]
133
134
/**
135
* Block and wait for completion (mainly for testing)
136
*/
137
def isCompleted: Boolean
138
}
139
140
object FutureOutcome {
141
/**
142
* Create from a Future[Outcome]
143
*/
144
def apply(future: Future[Outcome]): FutureOutcome
145
146
/**
147
* Create from a successful outcome
148
*/
149
def successful(outcome: Outcome): FutureOutcome
150
151
/**
152
* Create from a failed outcome
153
*/
154
def failed(exception: Throwable): FutureOutcome
155
156
/**
157
* Create from a canceled outcome
158
*/
159
def canceled(exception: Throwable): FutureOutcome
160
}
161
```
162
163
**Usage Examples:**
164
165
```scala
166
import org.scalatest._
167
import scala.concurrent.Future
168
import scala.util.{Success, Failure}
169
170
// Creating FutureOutcome instances
171
val successfulOutcome = FutureOutcome.successful(Succeeded)
172
val failedOutcome = FutureOutcome.failed(new RuntimeException("Test failed"))
173
174
// Transforming outcomes
175
val transformedOutcome = successfulOutcome.map {
176
case Succeeded => Succeeded
177
case failed => failed
178
}
179
180
// Chaining operations
181
val chainedOutcome = successfulOutcome.flatMap { outcome =>
182
if (outcome == Succeeded) {
183
FutureOutcome.successful(Succeeded)
184
} else {
185
FutureOutcome.failed(new RuntimeException("Chain failed"))
186
}
187
}
188
189
// Error recovery
190
val recoveredOutcome = failedOutcome.recover {
191
case ex: RuntimeException => Failed(ex)
192
case other => Failed(other)
193
}
194
```
195
196
### Recovery Methods
197
198
Utilities for handling exceptions in asynchronous tests.
199
200
```scala { .api }
201
trait RecoverMethods {
202
203
/**
204
* Recover from specific exception types in async operations
205
*/
206
def recoverToSucceededIf[T <: AnyRef](future: Future[Any])(implicit classTag: ClassTag[T]): Future[Assertion]
207
208
/**
209
* Recover expecting a specific exception to be thrown
210
*/
211
def recoverToExceptionIf[T <: AnyRef](future: Future[Any])(implicit classTag: ClassTag[T]): Future[T]
212
}
213
```
214
215
**Usage Examples:**
216
217
```scala
218
import org.scalatest.RecoverMethods
219
import scala.concurrent.Future
220
221
class AsyncRecoveryExample extends AsyncFunSuite with RecoverMethods {
222
223
test("should recover from expected exception") {
224
val failingFuture = Future {
225
throw new IllegalArgumentException("Expected error")
226
}
227
228
// Test succeeds if the expected exception is thrown
229
recoverToSucceededIf[IllegalArgumentException] {
230
failingFuture
231
}
232
}
233
234
test("should capture expected exception") {
235
val failingFuture = Future {
236
throw new RuntimeException("Test error")
237
}
238
239
// Capture the exception for further assertions
240
recoverToExceptionIf[RuntimeException] {
241
failingFuture
242
}.map { exception =>
243
exception.getMessage should include ("Test error")
244
}
245
}
246
}
247
```
248
249
### Complete Lastly
250
251
Trait for cleanup operations that run after async tests complete.
252
253
```scala { .api }
254
trait CompleteLastly {
255
256
/**
257
* Register cleanup code to run after test completion
258
*/
259
def completeLastly(completeLastlyCode: => Unit): Unit
260
261
/**
262
* Register async cleanup code to run after test completion
263
*/
264
def completeLastly(completeLastlyCode: => Future[Unit]): Unit
265
}
266
```
267
268
**Usage Examples:**
269
270
```scala
271
class AsyncResourceExample extends AsyncFunSuite with CompleteLastly {
272
273
test("should cleanup resources after async test") {
274
val resource = acquireResource()
275
276
// Register cleanup that runs regardless of test outcome
277
completeLastly {
278
resource.close()
279
println("Resource cleaned up")
280
}
281
282
// Test code that uses the resource
283
Future {
284
resource.process()
285
assert(resource.isProcessed)
286
}
287
}
288
289
test("should handle async cleanup") {
290
val asyncResource = acquireAsyncResource()
291
292
// Register async cleanup
293
completeLastly {
294
asyncResource.cleanupAsync()
295
}
296
297
asyncResource.processAsync().map { result =>
298
assert(result.isSuccess)
299
}
300
}
301
}
302
```
303
304
### Async Fixtures
305
306
Support for asynchronous setup and teardown in test fixtures.
307
308
```scala { .api }
309
/**
310
* Async version of fixture test suites
311
*/
312
trait AsyncFixtureTestSuite extends AsyncTestSuite {
313
type FixtureParam
314
315
/**
316
* Async fixture method that provides test data
317
*/
318
def withAsyncFixture(test: OneArgAsyncTest): FutureOutcome
319
}
320
321
/**
322
* One argument async test function
323
*/
324
abstract class OneArgAsyncTest extends (FixtureParam => Future[Outcome]) with TestData {
325
def apply(fixture: FixtureParam): Future[Outcome]
326
}
327
```
328
329
**Usage Examples:**
330
331
```scala
332
import scala.concurrent.Future
333
334
class AsyncFixtureExample extends AsyncFunSuite {
335
336
case class DatabaseConnection(url: String) {
337
def query(sql: String): Future[List[String]] = Future.successful(List("result1", "result2"))
338
def close(): Future[Unit] = Future.successful(())
339
}
340
341
type FixtureParam = DatabaseConnection
342
343
def withAsyncFixture(test: OneArgAsyncTest): FutureOutcome = {
344
val connection = DatabaseConnection("jdbc:test://localhost")
345
346
complete {
347
withFixture(test.toNoArgAsyncTest(connection))
348
} lastly {
349
// Async cleanup
350
connection.close()
351
}
352
}
353
354
test("should query database asynchronously") { connection =>
355
connection.query("SELECT * FROM users").map { results =>
356
results should have size 2
357
results should contain ("result1")
358
}
359
}
360
}
361
```
362
363
## Types
364
365
```scala { .api }
366
/**
367
* Test outcome for async operations
368
*/
369
sealed abstract class Outcome extends Product with Serializable
370
case object Succeeded extends Outcome
371
final case class Failed(exception: Throwable) extends Outcome
372
final case class Canceled(exception: Throwable) extends Outcome
373
case object Pending extends Outcome
374
375
/**
376
* Async test registration
377
*/
378
trait AsyncTestRegistration {
379
def registerAsyncTest(testName: String, testTags: Tag*)(testFun: => Future[compatible.Assertion]): Unit
380
def registerIgnoredAsyncTest(testName: String, testTags: Tag*)(testFun: => Future[compatible.Assertion]): Unit
381
}
382
383
/**
384
* Test data for async tests
385
*/
386
trait AsyncTestDataInvoker {
387
def apply(testData: TestData): Future[Outcome]
388
}
389
```