• Stars
    star
    315
  • Rank 132,951 (Top 3 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 1 year ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Useful, testable Swift concurrency.

swift-concurrency-extras

CI Slack

Reliably testable Swift concurrency.

Learn more

This library was designed to support libraries and episodes produced for Point-Free, a video series exploring the Swift programming language hosted by Brandon Williams and Stephen Celis.

You can watch all of the episodes here.

video poster image

Motivation

This library comes with a number of tools that make working with Swift concurrency easier and more testable.

ActorIsolated and LockIsolated

The ActorIsolated and LockIsolated types help wrap other values in an isolated context. ActorIsolated wraps the value in an actor so that the only way to access and mutate the value is through an async/await interface. LockIsolated wraps the value in a class with a lock, which allows you to read and write the value with a synchronous interface. You should prefer to use ActorIsolated when you have access to an asynchronous context.

Streams

The library comes with numerous helper APIs spread across the two Swift stream types:

  • There are helpers that erase any AsyncSequence conformance to either concrete stream type. This allows you to treat the stream type as a kind of "type erased" AsyncSequence.

    For example, suppose you have a dependency client like this:

    struct ScreenshotsClient {
      var screenshots: () -> AsyncStream<Void>
    }

    Then you can construct a live implementation that "erases" the NotificationCenter.Notifications async sequence to a stream:

    extension ScreenshotsClient {
      static let live = Self(
        screenshots: {
          NotificationCenter.default
            .notifications(named: UIApplication.userDidTakeScreenshotNotification)
            .map { _ in }
            .eraseToStream()  // โฌ…๏ธ
        }
      )
    }

    Use eraseToThrowingStream() to propagate failures from throwing async sequences.

  • Swift 5.9's makeStream(of:) functions have been back-ported. It can handy in tests that need to override a dependency endpoint that returns a stream:

    let screenshots = AsyncStream.makeStream(of: Void.self)
    
    let model = FeatureModel(screenshots: { screenshots.stream })
    
    XCTAssertEqual(model.screenshotCount, 0)
    screenshots.continuation.yield()  // Simulate a screenshot being taken.
    XCTAssertEqual(model.screenshotCount, 1)
  • Static AsyncStream.never and AsyncThrowingStream.never helpers are provided that represent streams that live forever and never emit. They can be handy in tests that need to override a dependency endpoint with a stream that should suspend and never emit for the duration of the test.

    let model = FeatureModel(screenshots: { .never })
  • Static AsyncStream.finished and AsyncThrowingStream.finished(throwing:) helpers are provided that represents streams that complete immediately without emitting. They can be handy in tests that need to override a dependency endpoint with a stream that completes/fails immediately.

Tasks

The library enhances the Task type with new functionality.

  • The static function Task.never() can asynchronously return a value of any type, but does so by suspending forever. This can be useful for satisfying a dependency requirement in a way that does not require you to actually return data from that endpoint.

    For example, suppose you have a dependency client like this:

    struct SettingsClient {
      var fetchSettings: () async throws -> Settings
    }

    You can override the client's fetchSettings endpoint in tests to suspend forever by awaiting Task.never():

    SettingsClient(
      fetchSettings: { try await Task.never() }
    )
  • Task.cancellableValue is a property that awaits the unstructured task's value property while propagating cancellation from the current async context.

  • Task.megaYield() is a blunt tool that can make flakey async tests a little less flakey by suspending the current task a number of times and improve the odds that other async work has enough time to start. Prefer the reliability of serial execution instead where possible.

UncheckedSendable

A wrapper type that can make any type Sendable, but in an unsafe and unchecked way. This type should only be used as an alternative to @preconcurrency import, which turns off concurrency checks for everything in the library. Whereas UncheckedSendable allows you to turn off concurrency warnings for just one single usage of a particular type.

While SE-0302 mentions future work of "Adaptor Types for Legacy Codebases", including an UnsafeTransfer type that serves the same purpose, it has not landed in Swift.

Serial execution

Some asynchronous code is notoriously difficult to test in Swift due to how suspension points are processed by the runtime. The library comes with a static function, withMainSerialExecutor, that attempts to run all tasks spawned in an operation serially and deterministically. This function can be used to make asynchronous tests faster and less flakey.

Warning: This API is only intended to be used from tests to make them more reliable. Please do not use it from application code.

We say that it "attempts to run all tasks spawned in an operation serially and deterministically" because under the hood it relies on a global, mutable variable in the Swift runtime to do its job, and there are no scoping guarantees should this mutable variable change during the operation.

For example, consider the following seemingly simple model that makes a network request and manages some isLoading state while the request is inflight:

@Observable
class NumberFactModel {
  var fact: String?
  var isLoading = false
  var number = 0

  // Inject the request dependency explicitly to make it testable, but can also
  // be provided via a dependency management library.
  let getFact: (Int) async throws -> String

  func getFactButtonTapped() async {
    self.isLoading = true
    defer { self.isLoading = false }
    do {
      self.fact = try await self.getFact(self.number)
    } catch {
      // TODO: Handle error
    }
  }
}

We would love to be able to write a test that allows us to confirm that the isLoading state flips to true and then false. You might hope that it is as easy as this:

func testIsLoading() async {
  let model = NumberFactModel(getFact: { 
    "\($0) is a good number." 
  })

  let task = Task { await model.getFactButtonTapped() }
  XCTAssertEqual(model.isLoading, true)
  XCTAssertEqual(model.fact, nil)

  await task.value
  XCTAssertEqual(model.isLoading, false)
  XCTAssertEqual(model.fact, "0 is a good number.")
}

However this fails almost 100% of the time. The problem is that the line immediately after creating the unstructured Task executes before the line inside the unstructured task, and so we never detect the moment the isLoading state flips to true.

You might hope you can wiggle yourself in between the moment the getFactButtonTapped method is called and the moment the request finishes by using a Task.yield:

 func testIsLoading() async {
   let model = NumberFactModel(getFact: { 
     "\($0) is a good number." 
   })

   let task = Task { await model.getFactButtonTapped() }
+  await Task.yield()
   XCTAssertEqual(model.isLoading, true)
   XCTAssertEqual(model.fact, nil)

   await task.value
   XCTAssertEqual(model.isLoading, false)
   XCTAssertEqual(model.fact, "0 is a good number.")
 }

But that still fails the vast majority of times.

These problems, and more, can be fixed by running this entire test on the main serial executor. You will also have insert a small yield in the getFact endpoint due to Swift's ability to inline async closures that do not actually perform async work:

 func testIsLoading() async {
+  await withMainSerialExecutor {
     let model = NumberFactModel(getFact: {
+      await Task.yield()
       return "\($0) is a good number." 
     })

     let task = Task { await model.getFactButtonTapped() }
     await Task.yield()
     XCTAssertEqual(model.isLoading, true)
     XCTAssertEqual(model.fact, nil)

     await task.value
     XCTAssertEqual(model.isLoading, false)
     XCTAssertEqual(model.fact, "0 is a good number.")
+  }
 }

That small change makes this test pass deterministically, 100% of the time.

Documentation

The latest documentation for this library is available here.

Credits and thanks

Thanks to Pat Brown and Thomas Grapperon for providing feedback on the library before its release. Special thanks to Kabir Oberai who helped us work around an Xcode bug and ship serial execution tools with the library.

Other libraries

Concurrency Extras is just one library that makes it easier to write testable code in Swift.

  • Case Paths: Tools for working with and testing enums.

  • Clocks: A few clocks that make working with Swift concurrency more testable and more versatile.

  • Combine Schedulers: A few schedulers that make working with Combine more testable and more versatile.

  • Composable Architecture: A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.

  • Custom Dump: A collection of tools for debugging, diffing, and testing your application's data structures.

  • Dependencies: A dependency management library inspired by SwiftUI's "environment."

  • Snapshot Testing: Assert on your application by recording and and asserting against artifacts.

  • XCTest Dynamic Overlay: Call XCTFail and other typically test-only helpers from application code.

License

This library is released under the MIT license. See LICENSE for details.

More Repositories

1

swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
Swift
12,150
star
2

swift-snapshot-testing

๐Ÿ“ธ Delightful Swift snapshot testing.
Swift
3,737
star
3

isowords

Open source game built in SwiftUI and the Composable Architecture.
Swift
2,667
star
4

swift-navigation

Bringing simple and powerful navigation tools to all Swift platforms, inspired by SwiftUI.
Swift
1,950
star
5

swift-dependencies

A dependency management library inspired by SwiftUI's "environment."
Swift
1,527
star
6

swift-tagged

๐Ÿท A wrapper type for safer, expressive code.
Swift
1,364
star
7

swift-overture

๐ŸŽผ A library for function composition.
Swift
1,139
star
8

pointfreeco

๐ŸŽฌ The source for www.pointfree.co, a video series on functional programming and the Swift programming language.
Swift
1,098
star
9

episode-code-samples

๐Ÿ’พ Point-Free episode code.
Swift
950
star
10

swift-case-paths

๐Ÿงฐ Case paths extends the key path hierarchy to enum cases.
Swift
905
star
11

swift-parsing

A library for turning nebulous data into well-structured data, with a focus on composition, performance, generality, and ergonomics.
Swift
847
star
12

swift-nonempty

๐ŸŽ A compile-time guarantee that a collection contains a value.
Swift
839
star
13

swift-custom-dump

A collection of tools for debugging, diffing, and testing your application's data structures.
Swift
795
star
14

swift-html

๐Ÿ—บ A Swift DSL for type-safe, extensible, and transformable HTML documents.
Swift
760
star
15

combine-schedulers

โฐ A few schedulers that make working with Combine more testable and more versatile.
Swift
701
star
16

swift-perception

Observable tools, backported.
Swift
536
star
17

swift-identified-collections

A library of data structures for working with collections of identifiable elements in an ergonomic, performant way.
Swift
529
star
18

swift-web

๐Ÿ•ธ A collection of Swift server-side frameworks for handling HTML, CSS, routing and middleware.
Swift
481
star
19

swift-prelude

๐ŸŽถ A collection of types and functions that enhance the Swift language.
Swift
469
star
20

swift-validated

๐Ÿ›‚ A result type that accumulates multiple errors.
Swift
392
star
21

swift-issue-reporting

Report issues in your application and library code as Xcode runtime warnings, breakpoints, assertions, and do so in a testable manner.
Swift
361
star
22

swift-url-routing

A bidirectional router with more type safety and less fuss.
Swift
347
star
23

swift-gen

๐ŸŽฑ Composable, transformable, controllable randomness.
Swift
266
star
24

swift-macro-testing

Magical testing tools for Swift macros.
Swift
263
star
25

swift-clocks

โฐ A few clocks that make working with Swift concurrency more testable and more versatile.
Swift
258
star
26

syncups

A rebuild of Appleโ€™s โ€œScrumdingerโ€ application using modern, best practices for SwiftUI development.
Swift
205
star
27

swift-enum-properties

๐Ÿค Struct and enum data access in harmony.
Swift
200
star
28

composable-core-location

A library that bridges the Composable Architecture and Core Location.
Swift
108
star
29

vapor-routing

A bidirectional Vapor router with more type safety and less fuss.
Swift
89
star
30

swift-html-vapor

๐Ÿ’ง Vapor plugin for type-safe, transformable HTML views.
Swift
84
star
31

swift-playground-templates

๐Ÿซ A collection of helpful Xcode playground templates.
Makefile
81
star
32

pointfreeco-server

Point-Free server code.
40
star
33

TrySyncUps

The starting project for our try! Swift 2024 Composable Architecture workshop.
Swift
38
star
34

composable-core-motion

A library that bridges the Composable Architecture and Core Motion.
Swift
29
star
35

swift-boundaries

๐Ÿฃ Functional core, imperative shell.
Swift
28
star
36

swift-quickcheck

๐Ÿ An implementation of QuickCheck in Swift.
Swift
25
star
37

swift-either

For those times you want A or B!
Swift
21
star
38

swift-algebras

Algebraic laws bundled into concrete data types.
20
star
39

swift-parser-printer

โ†”๏ธ Parsing and printing
Swift
15
star
40

swift-html-kitura

โ˜๏ธ Kitura plugin for type-safe, transformable HTML views.
Swift
14
star
41

swiftui-navigation

This package is now Swift Navigation:
Swift
13
star
42

homebrew-swift

Ruby
3
star
43

swift-bugs

3
star
44

Ccmark

Swift
2
star
45

.github

1
star