swift-clocks

repository·main·Indexed 43 days ago

https://github.com/pointfreeco/swift-clocks

Specialized implementations of the Swift Clock protocol to make time-based asynchronous code more testable and predictable. Includes TestClock for deterministic testing, ImmediateClock for speeding up Xcode Previews, and UnimplementedClock to verify dependency usage. Provides AnyClock for concrete erasure and SwiftUI environment values for clock injection.

Tokens
2.5K
Snippets
11
Records
16
Agent score
56%

What's inside swift-clocks

  1. Available Clock implementations

    main

    The library provides three primary implementations of the Clock protocol to handle different development scenarios:

    • ImmediateClock: Executes time-based operations immediately.
    • TestClock: Allows for manual control over time progression, making it ideal for testing asynchronous logic without waiting for real-world time.
    • UnimplementedClock: Used to represent a clock that has not yet been implemented, useful for scaffolding or identifying missing dependencies.
  2. Use the Clock protocol for testable time-based asynchrony

    main
    The Clock protocol provides an abstraction for time-based asynchrony in Swift's structured concurrency. Instead of using Task.sleep directly, which forces code to wait for real-world time, you should use a Clock implementation. This allows you to swap real-time clocks with specialized implementations like TestClock for deterministic testing or ImmediateClock for instant execution.
  3. Use `TestClock` for deterministic time-based testing

    main

    A TestClock allows you to control time deterministically in your tests. It is ideal for testing asynchronous code that relies on sleep or time-based operators like debounce, throttle, and timeout. Instead of waiting for real-world time to pass, you can manually advance the clock to trigger specific time-based behaviors instantly and reliably.

    func testTimer() async {
      let clock = TestClock()
      let model = FeatureModel(clock: clock)
    
      XCTAssertEqual(model.count, 0)
      model.startTimerButtonTapped()
    
      // Advance the clock 1 second and prove that the model's
      // count incremented by one.
      await clock.advance(by: .seconds(1))
      XCTAssertEqual(model.count, 1)
    
      // Advance the clock 4 seconds and prove that the model's
      // count incremented by 4.
      await clock.advance(by: .seconds(4))
      XCTAssertEqual(model.count, 5)
    
      // Stop the timer, run the clock until there is no more
      // suspensions, and prove that the count did not increment.
      model.stopTimerButtonTapped()
      await clock.run()
      XCTAssertEqual(model.count, 5)
    }
  4. Use `UnimplementedClock` to verify dependency usage in tests

    main

    An UnimplementedClock causes an XCTest failure if any of its endpoints are invoked. Use this when a feature requires a clock dependency, but you want to prove that the specific execution flow being tested does not actually use any time-based asynchrony. If the code accidentally uses the clock, the test will fail, alerting you to unexpected behavior.

    func testIncrementDecrement() {
      let model = FeatureModel(clock: UnimplementedClock())
    
      XCTAssertEqual(model.count, 0)
      self.model.incrementButtonTapped()
      XCTAssertEqual(model.count, 1)
      self.model.decrementButtonTapped()
      XCTAssertEqual(model.count, 0)
    }
  5. Use `ImmediateClock` to speed up Xcode Previews

    main

    An ImmediateClock squashes time down to a single instant, causing any sleep calls to execute immediately. This is highly useful for SwiftUI Previews where you want to avoid waiting for real-world timers (e.g., a 5-second welcome message) to finish before seeing UI changes.

    struct Feature_Previews: PreviewProvider {
      static var previews: some View {
        Feature(clock: ImmediateClock())
      }
    }
  6. Create timers using `clock.timer(interval:)`

    main

    All clocks in this library provide a timer(interval:) method that returns an AsyncSequence. This allows you to handle periodic tasks using a simple for await loop.

    for await _ in self.clock.timer(interval: .seconds(1)) {
      self.count += 1
    }
  7. Check for clock suspension in TestClock

    main

    When using TestClock, you can verify if the clock is currently suspended (i.e., time is not advancing). This is useful for ensuring that tasks are correctly scheduled or that a process has paused as expected.

    • checkSuspension(): Throws a SuspensionError if the clock is not in a suspended state.
    • SuspensionError: The error thrown when checkSuspension() is called on a clock that is not suspended.
    let clock = TestClock()
    // ... perform actions ...
    try clock.checkSuspension()
  8. Inject clocks into SwiftUI via Environment values

    main

    The library provides continuousClock and suspendingClock environment values to easily propagate clocks through a SwiftUI view hierarchy. This allows you to use a real ContinuousClock in production and an ImmediateClock in previews.

    struct Feature: View {
      @Environment(\.continuousClock) var clock
    
      var body: some View {
        // ... use self.clock.sleep(for: ...) 
      }
    }
    
    struct Feature_Previews: PreviewProvider {
      static var previews: some View {
        Feature()
          .environment(\.continuousClock, ImmediateClock())
      }
    }
  9. Control time with TestClock

    main

    A TestClock provides methods to manually manipulate the current time, allowing you to trigger scheduled tasks or simulate delays in your asynchronous code.

    • advance(by:): Advances the clock by a specific duration.
    • advance(to:): Advances the clock to a specific point in time.
    • run(timeout:file:line:): Runs a block of code and fails if the code does not complete within the specified timeout relative to the clock's time.
    let clock = TestClock()
    
    // Advance time by 5 seconds
    await clock.advance(by: .seconds(5))
    
    // Advance time to a specific instant
    await clock.advance(to: .now + .seconds(10))
    
    // Run a task with a timeout
    try await clock.run(timeout: .seconds(1)) {
        // asynchronous work here
    }
  10. Create an ImmediateClock

    main

    An ImmediateClock is a clock that always returns the same instant for every call to now(). It is useful for testing scenarios where you want to simulate time passing without actually waiting, or when you need a stable reference point for time-based logic.

    You can create an ImmediateClock using the default initializer or by providing a specific starting instant.

    // Create an immediate clock starting at the current time
    let clock = ImmediateClock()
    
    // Create an immediate clock starting at a specific instant
    let specificInstant = ContinuousClock.now
    let clockWithInstant = ImmediateClock(now: specificInstant)