Skip to content

Commit 1c1a67a

Browse files
committed
Create an Exit Testing DocC article (no rly)
1 parent 76eb534 commit 1c1a67a

File tree

1 file changed

+136
-0
lines changed

1 file changed

+136
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Exit testing
2+
3+
<!--
4+
This source file is part of the Swift.org open source project
5+
6+
Copyright (c) 2023–2025 Apple Inc. and the Swift project authors
7+
Licensed under Apache License v2.0 with Runtime Library Exception
8+
9+
See https://swift.org/LICENSE.txt for license information
10+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
11+
-->
12+
13+
Use exit tests to test functionality that may cause a test process to terminate.
14+
15+
## Overview
16+
17+
Your code may contain calls to [`precondition()`](https://developer.apple.com/documentation/swift/precondition(_:_:file:line:)),
18+
[`fatalError()`](https://developer.apple.com/documentation/swift/fatalerror(_:file:line:)),
19+
or other functions that may cause the current process to terminate. For example:
20+
21+
```swift
22+
extension Customer {
23+
func eat(_ food: consuming some Food) {
24+
precondition(food.isDelicious, "Tasty food only!")
25+
precondition(food.isNutritious, "Healthy food only!")
26+
...
27+
}
28+
}
29+
```
30+
31+
In this function, if `food.isDelicious` or `food.isNutritious` is `false`, the
32+
precondition will fail and Swift will terminate the process. You can write an
33+
exit test to validate preconditions like the ones above and to make sure that
34+
your functions correctly catch invalid inputs.
35+
36+
## Create an exit test
37+
38+
To create an exit test, call either the ``expect(exitsWith:observing:_:sourceLocation:performing:)``
39+
or the ``require(exitsWith:observing:_:sourceLocation:performing:)`` macro and
40+
pass a closure containing the code that may terminate the process along with the
41+
expected result of calling that code (success, failure, or a specific exit code
42+
or signal):
43+
44+
```swift
45+
@Test func `Customer won't eat food unless it's delicious`() async {
46+
let result = await #expect(exitsWith: .failure) {
47+
var food = ...
48+
food.isDelicious = false
49+
Customer.current.eat(food)
50+
}
51+
}
52+
```
53+
54+
When an exit test is performed at runtime, the testing library starts a new
55+
process with the same executable as the current process. The current task is
56+
then suspended (as with `await`) and waits for the child process to terminate.
57+
`expression` is not called in the parent process.
58+
59+
Meanwhile, in the child process, the closure you passed to ``expect(exitsWith:observing:_:sourceLocation:performing:)``
60+
or to ``require(exitsWith:observing:_:sourceLocation:performing:)`` is called
61+
directly. To ensure a clean environment for execution, the closure is not called
62+
within the context of the original test. Instead, it is treated as if it were
63+
the `main()` function of the child process.
64+
65+
If the closure returns before the child process terminates, it is terminated
66+
automatically (as if the main function of the child process were allowed to
67+
return naturally.) If an error is thrown from the closure, it is handed as if
68+
the error were thrown from `main()` and the process is terminated.
69+
70+
Once the child process terminates, the parent process resumes and compares its
71+
exit status against the expected exit condition you passed. If they match, the
72+
exit test has passed; otherwise, it has failed and an issue is recorded.
73+
74+
## Gather output from the child process
75+
76+
By default, the child process is configured without a standard output or
77+
standard error stream. If your test needs to review the content of either of
78+
these streams, you can pass its key path in the `observedValues` argument:
79+
80+
```swift
81+
extension Customer {
82+
func eat(_ food: consuming some Food) {
83+
print("Let's see if I want to eat \(food)...")
84+
precondition(food.isDelicious, "Tasty food only!")
85+
precondition(food.isNutritious, "Healthy food only!")
86+
...
87+
}
88+
}
89+
90+
@Test func `Customer won't eat food unless it's delicious`() async {
91+
let result = await #expect(
92+
exitsWith: .failure,
93+
observing: [\.standardOutputContent]
94+
) {
95+
var food = ...
96+
food.isDelicious = false
97+
Customer.current.eat(food)
98+
}
99+
if let result {
100+
#expect(result.standardOutputContent.contains(UInt8(ascii: "L")))
101+
}
102+
}
103+
```
104+
105+
- Note: The content of the standard output and standard error streams may
106+
contain any arbitrary sequence of bytes, including sequences that are not
107+
valid UTF-8 and cannot be decoded by [`String.init(cString:)`](https://developer.apple.com/documentation/swift/string/init(cstring:)-6kr8s).
108+
These streams are globally accessible within the child process, and any code
109+
running in an exit test may write to it including the operating system and any
110+
third-party dependencies you have declared in your package.
111+
112+
The actual exit condition of the child process is always reported by the testing
113+
library even if you do not specify it in `observedValues`.
114+
115+
## Constraints on exit tests
116+
117+
### State cannot be captured
118+
119+
Exit tests cannot capture any state originating in the parent process or from
120+
the enclosing lexical context. For example, the following exit test will fail to
121+
compile because it captures a variable declared outside the exit test itself:
122+
123+
```swift
124+
@Test func `Customer won't eat food unless it's nutritious`() async {
125+
let isNutritious = false
126+
await #expect(exitsWith: .failure) {
127+
var food = ...
128+
food.isNutritious = isNutritious // ❌ ERROR: trying to capture state here
129+
Customer.current.eat(food)
130+
}
131+
}
132+
```
133+
134+
### Exit tests cannot be nested
135+
136+
An exit test cannot run within another exit test.

0 commit comments

Comments
 (0)