Skip to content

Instantly share code, notes, and snippets.

@janodev
Last active June 12, 2024 09:42
Show Gist options
  • Save janodev/ebff9d825e21a238fa5c1720a91fef20 to your computer and use it in GitHub Desktop.
Save janodev/ebff9d825e21a238fa5c1720a91fef20 to your computer and use it in GitHub Desktop.
Summary of the Testing framework presented in WWDC 2024
/*
Sessions:
- Meet Swift Testing
https://www.youtube.com/watch?v=WFnkNcvLnCI
- Go further with Swift Testing
https://www.youtube.com/watch?v=bOvWGHi-BxI
There are three assertions that replace all XCTAssert functions:
#expect()
#expect(throws:)
#require()
*/
// test passes if expression returns true
#expect(Color.black.toHex() == "000000")
// test passes if expression throws
#expect(throws: ThemeError.self) {
try convertToHex(Color.black)
}
// test ends early if value is nil. Similar to XCTUnwrap.
_ = try #require(Int("1"))
// another way
do {
_ = try convertToHex(Color.black)
} catch ThemeError.wrongColor {
} catch {
Issue.record(error, "Unexpected error")
}
// test passes if #require fails
withKnownIssue {
_ = try #require(Int("A"))
}
// ORGANIZING TESTS
// All assertions must be inside a function annotated with @Test
@Test
func colorToHex() throws {
#expect(Color.black.toHex() == "000000")
}
// A test function may be global or be grouped inside a struct, actor, or class.
struct Colors {
@Test
func colorToHex() throws {
#expect(Color.black.toHex() == "000000")
}
}
// ORGANIZING TESTS IN SUITES
// Optionally you can label them adding @Suite.
@Suite("A test demonstration")
struct TestSuite {
// ...
}
// Test run in parallel and random order unless you pass '.serialized'.
@Suite("A test demonstration", .serialized)
struct TestSuite {
// nested suites will inherit the .serialized argument
@Suite struct TestSuite { ... }
}
// ORGANIZING TESTS WITH TAGS
// Extend Tag to create custom tags
extension Tag {
@Tag static var caffeinated: Self
@Tag static var chocolatey: Self
}
// then you can tag suites and tests
@Suite(.tags(.caffeinated))
struct OneMoreSuite {
@Test(.tags(.caffeinated, .chocolatey))
func whatever() {/*...*/}
}
// Recap: test can be organized using Suites, nested Suites, and tags.
// Use filter in the test navigator and Test plans to select which tests to run.
// TEST TRAITS
// @Test("Custom name") Custom name
// @Test(.bug("myjira.com/issues/999", "Title") Related bug report
// @Test(.tags(.critical)) Custom tag
// @Test(.enabled(if: Server.isOnline)) Enabled by runtime condition
// @Test(.disabled("Currently broken")) Disabled
// @Test(.timeLimit(.minutes(3))) Maximum time
// @Test @available(macOS 15, *) Limit the test to certain OS versions
// PARAMETERIZING FUNCTIONS
// This calls the function three times.
// (first enum case passed as argument needed the explicit type, the rest were inferred)
@Test(arguments: [Flavor.vanilla, .chocolate, .strawberry])
func doesNotContainNuts1(flavor: Flavor) throws {
try #require(!flavor.containsNuts)
}
// Noteworthy:
// - Enums can also be passed as allCases if they support CaseIterable.
// - You may also pass Array, Set, OptionSet, Dictionary, and Range.
// - Tests can be parameterized with a maximum of two collections.
// This calls with all permutations of the possible values for each argument.
@Test(arguments: Flavor.allCases, Dish.allCases)
func doesNotContainNuts2(flavor: Flavor, dish: Dish) throws {
try #require(!flavor.containsNuts)
}
// This makes pairs, then calls with each pair.
@Test(arguments: zip(Flavor.allCases, Dish.allCases))
func doesNotContainNuts2(flavor: Flavor, dish: Dish) throws {
try #require(!flavor.containsNuts)
}
// TESTING ASYNCHRONOUS CONDITIONS
// testing async calls
@Test func bakeCookies() async throws {
let cookies = await Cookie.bake(count: 10)
#expect(cookies.count == 10)
}
// testing completion handlers
@Test func bakeCookies() async throws {
let cookies = await Cookie.bake(count: 10)
try await withCheckedThrowingContinuation { continuation in
eat(cookies, with: .milk) { result, error in
if let result {
continuation.resume(returning: result)
} else if let error {
continuation.resume(throwing: error)
}
}
}
}
// testing number of calls
@Test func bakeCookies() async throws {
let cookies = await Cookie.bake(count: 10)
try await confirmation("Ate cookies", expectedCount: 0) { ateCookie in
try await eat(cookies, with: .milk) { cookie, crumbs in
ateCookie()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment