Skip to content

Behavior validation docs #408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
df277e7
Add detail to the API collection for expectations.
iamleeg May 8, 2024
00f66c6
Add an article on testing for errors
iamleeg May 8, 2024
8a91324
An article on asynchronous code
iamleeg May 9, 2024
cede7da
Add copyright boilerplate to the new files
iamleeg May 9, 2024
d3f3cca
Link to expect and require macros on first mention.
iamleeg May 10, 2024
8192e0f
Hard word wrap paragraphs.
iamleeg May 10, 2024
a2bf9c0
Specify which error the require macro throws.
iamleeg May 10, 2024
a0dfc5b
Rename Swift Testing -> swift-testing
iamleeg May 10, 2024
9c1e5dd
Separate out asynchronous calculation from test of result.
iamleeg May 10, 2024
8027b83
Replace "block" with "closure"
iamleeg May 10, 2024
864fc9b
Describe that a throwing test function detects errors and fails
iamleeg May 10, 2024
1876270
Replace tags with attributes in exception-testing example
iamleeg May 10, 2024
1cb63d0
Rewrite the error examples as to use a food truck-related example.
iamleeg May 13, 2024
03aee83
Rewrite async testing examples to look like a food truck
iamleeg May 13, 2024
a609530
Replace the API collection code snippet with a food truck example.
iamleeg May 13, 2024
59861d7
Replace final example with FoodTruck-related example.
iamleeg May 15, 2024
e32836f
Remove the note about #require(throws: Never.self).
iamleeg May 15, 2024
1074cbc
Remove effecting function from #expect macro body.
iamleeg May 15, 2024
1811c9f
Replace #expect() and #require() with double-backtick links
iamleeg May 15, 2024
a5134ac
Add "asynchronous" to the abstract for Expectations
iamleeg May 17, 2024
e594612
Fix line-wrapping.
iamleeg May 17, 2024
a9a17b5
Remove double spaces after periods. Except that one.
iamleeg May 17, 2024
fa9c4c2
Suggested changes to asynchronous code article.
iamleeg May 17, 2024
9f5d89a
Resolve issues in the article on errors.
iamleeg May 17, 2024
9eae71e
2 spaces in code snippet indentation
iamleeg May 23, 2024
9fe5b3f
Address recent feedback.
iamleeg May 23, 2024
a53d724
Swap the expected and actual values in the #expect example
iamleeg May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions Sources/Testing/Testing.docc/Expectations.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,54 @@ See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
-->

Check for expected values and outcomes in tests.
Check for expected values, outcomes, and events in tests.

## Overview

The testing library provides `#expect()` and `#require()` macros you use to
validate expected outcomes. To validate that an error is thrown, or _not_ thrown,
the testing library provides several overloads of the macros that you can use.
Use a ``Confirmation`` to confirm the occurrence of an asynchronous event that
you can't check directly using an expectation.
Use ``expect(_:_:sourceLocation:)`` and
``require(_:_:sourceLocation:)-5l63q`` macros to validate expected
outcomes. To validate that an error is thrown, or _not_ thrown, the
testing library provides several overloads of the macros that you can
use. Use a ``Confirmation`` to confirm the occurrence of an
asynchronous event that you can't check directly using an expectation.

### Validate your code's result

To validate that your code produces an expected value, use
``expect(_:_:sourceLocation:)``. ``expect(_:_:sourceLocation:)`` captures the
expression you pass, and provides detailed information when the code doesn't
satisfy the expectation:

```swift
class OrderCalculator {
func total(of subtotals: [Int]) -> Int {
return subtotals.reduce(1) { partialResult, subtotal in
partialResult + subtotal
}
}
}

@Test func calculatingOrderTotal() {
let calculator = OrderCalculator()
#expect(calculator.total(of: [3, 3]) == 6)
// Prints "Expectation failed: (calculator.total(of: [3, 3]) → 7) == 6"
}
```

Your test keeps running after ``expect(_:_:sourceLocation:)`` fails. To stop
the test when the code doesn't satisfy a requirement, use
``require(_:_:sourceLocation:)-5l63q`` instead:

```swift
@Test func returningCustomerRemembersUsualOrder() throws {
let customer = Customer(id: 123)
try #require(customer)
#expect(customer?.usualOrder?.countOfItems == 2) // The test runner doesn't reach this line if the customer is nil.
}
```

``require(_:_:sourceLocation:)-5l63q`` throws an instance of ``ExpectationFailedError`` when your code
fails to satisfy the requirement.

## Topics

Expand All @@ -30,6 +69,7 @@ you can't check directly using an expectation.

### Checking that errors are thrown

- <doc:testing-for-errors-in-swift-code>
- ``expect(throws:_:sourceLocation:performing:)-79piu``
- ``expect(throws:_:sourceLocation:performing:)-1xr34``
- ``expect(_:sourceLocation:performing:throws:)``
Expand All @@ -41,6 +81,7 @@ you can't check directly using an expectation.

### Confirming that asynchronous events occur

- ``<doc:testing-asynchronous-code>
- ``confirmation(_:expectedCount:fileID:filePath:line:column:_:)``
- ``Confirmation``

Expand Down
70 changes: 70 additions & 0 deletions Sources/Testing/Testing.docc/testing-asynchronous-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Testing asynchronous code

<!--
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
-->

Validate whether your code causes expected events to happen.

## Overview

`swift-testing` integrates with Swift concurrency, meaning that in many
situations you can test asynchronous code using standard Swift
features. Mark your test function as `async` and, in the function
body, `await` any asynchronous interactions:

```swift
@Test func priceLookupYieldsExpectedValue() async {
#expect(await unitPrice(for: .mozarella) == 3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because #expect(await ...) does not expand its arguments, please split this line up:

Suggested change
#expect(await unitPrice(for: .mozarella) == 3)
let mozarellaPrice = await unitPrice(for: .mozarella)
#expect(mozarellaPrice == 3)

The existing code will compile, but won't produce the best possible output on test failure.

}
```

In more complex situations, where the code you test doesn't use Swift
concurrency, you use ``Confirmation`` to discover whether an expected
event happens.

### Confirm that an event happens

If your code under test doesn't use Swift concurrency, call
``confirmation(_:expectedCount:fileID:filePath:line:column:_:)`` in
your asynchronous test function to create a `Confirmation` for the
expected event. In the trailing block parameter, call the code under
test. Swift Testing passes a `Confirmation` as the parameter to the
block, which you call as a function in the completion or event handler
for the code under test when the event you're testing for occurs:

```swift
@Test func orderCalculatorSuccessfullyCalculatesSubtotalForNoPizzas() async {
let calculator = OrderCalculator()
await confirmation() { confirmation in
calculator.successHandler = { _ in confirmation() }
calculator.subtotal(for: PizzaToppings(bases: []))
}
}
```

If you expect the event to happen more than once, set the
`expectedCount` parameter to the number of expected occurrences. The
test passes if the number of occurrences during the test matches the
expected count, and fails otherwise.

### Confirm that an event doesn't happen

To validate that a particular event doesn't occur during a test,
create a `Confirmation` with an expected count of `0`:

```swift
@Test func orderCalculatorEncountersNoErrors() async {
let calculator = OrderCalculator()
await confirmation(expectedCount: 0) { confirmation in
calculator.errorHandler = { _ in confirmation() }
calculator.subtotal(for: PizzaToppings(bases: []))
}
}
```
131 changes: 131 additions & 0 deletions Sources/Testing/Testing.docc/testing-for-errors-in-swift-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Testing for errors in Swift code

<!--
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
-->

Ensure that your code handles errors in the way you expect.

## Overview

The Swift language provides an idiomatic approach to [error
handling](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling),
based on throwing errors where your code detects a failure for a
caller to catch and react to.

Write tests for your code that validate the conditions in which the
code throws errors, and the conditions in which it returns without
throwing an error. Use overrides of the `#expect()` and
``require(_:_:sourceLocation:)-5l63q`` macros that check for errors.

### Validate that your code throws an expected error

The Swift structure in this example represents a list that accepts any
number of toppings for pizzas in the list. The API contains a method for
applying a topping to a range of pizzas, and a method for retrieving the
toppings requested for the item at a given index. Both of these methods
throw errors if their parameters are outside the list's range.

```swift
enum PizzaBase {
case deepCrust
case shallowCrust
case calzone
}

enum Topping {
case tomato
case cheese
case caper
case anchovy
case prosciutto
case pineapple
}

struct PizzaToppings {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a Toppings type and a PizzaToppings type. But this type also describes the base. Perhaps this should just be Pizza and the other types should be nested Pizza.Topping and Pizza.Base?

enum PizzaToppingsError : Error {
case outOfRange
}

let pizzas: [PizzaBase]
var toppings: [Int: [Topping]]

init(bases: [PizzaBase]) {
pizzas = bases
toppings = [Int: [Topping]]()
}

mutating func add(topping: Topping, toPizzasIn range: Range<Int>) throws {
guard Int(range.startIndex) >= 0 && Int(range.endIndex) < pizzas.count else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awkward phrasing. What are you trying to do here? We can probably simplify it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to test that the range is within the domain of indices in the pizzas array, but given that you go on below to say that I don't need to show the code under test, I'll avoid fixing this (which isn't a complete check anyway).

throw PizzaToppingsError.outOfRange
}
for index in range {
if var toppingList = toppings[index] {
toppingList.append(topping)
toppings[index] = toppingList
} else {
toppings[index] = [topping]
}
}
}

func toppings(forPizzaAt index: Int) throws -> [Topping] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of code here that isn't actually going to be used by developers. Are we sure we need to actually build out this type for the sake of explaining the topic? Is there some way to factor some of it out of the documentation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I'll remove it.

guard index >= 0 && index < pizzas.count else {
throw PizzaToppingsError.outOfRange
}
return toppings[index] ?? []
}

// Other methods.
}
```

Create a test function that `throws` and `try` the code under test.
If the code throws an error, then your test fails.

To check that the code under test throws a specific error, or to continue a
longer test function after the code throws an error, pass that error as the
first argument of ``expect(throws:_:sourcelocation:performing:)-1xr34``, and
pass a closure that calls the code under test:

```swift
@Test func cannotAddToppingToPizzaBeforeStartOfList() {
var order = PizzaToppings(bases: [.calzone, .deepCrust])
#expect(throws: PizzaToppings.PizzaToppingsError.outOfRange) {
try order.add(topping: .mozarella, toPizzasIn: -1..<0)
}
}
```

If the closure completes without throwing an error, the testing library
records an issue. Other overloads of ``expect(_:_:sourceLocation:)`` let you test that
the code throws an error of a given type, or matching an arbitrary
Boolean test. Similar overloads of ``require(_:_:sourceLocation:)-5l63q`` stop running your
test if the code doesn't throw the expected error.

### Validate that your code doesn't throw an error

Validate that the code under test doesn't throw an error by comparing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should emphasize here that a test function that throws will fail if an error is thrown, so usually you don't need to use #expect(throws: Never.self). You'd really only use it if you want to record an issue for a thrown error but not stop execution of the test function. It's pretty nuanced, to the point that I'm not sure we should even call it out in the documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some history here that we don't need to document, but for your own edification/amusement: XCTest in Objective-C has XCTAssertNoThrow() that asserts a given expression doesn't throw an Objective-C or C++ exception. This is different from a Swift error because if an exception is thrown through a test method's frame, it tends to wreak some havoc on the way out before being caught by XCTest, so you want to proactively catch them if you know they might occur. This was inherited as XCTAssertNoThrow() in Swift which checks if a function throws a Swift error rather than an exception.

the error to `Never`:

```swift
@Test func canAddToppingToPizzaInPositionZero() throws {
var order = PizzaToppings(bases: [.thinCrust, .thinCrust])
#expect(throws: Never.self) {
try order.add(topping: .caper, toPizzasIn: 0..<1)
}
let toppings = try order.toppings(forPizzaAt: 0)
#expect(toppings == [.caper])
}
```

If the closure throws an error, the testing library records an issue.
If you need the test to stop if the code throws an error, include the
code inline in the test function instead of wrapping it in an
``expect(throws:_:sourcelocation:performing:)-1xr34`` block.