Skip to content

Commit

Permalink
238
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Jun 19, 2023
1 parent d08b3bc commit 4b55f51
Show file tree
Hide file tree
Showing 17 changed files with 974 additions and 0 deletions.
5 changes: 5 additions & 0 deletions 0238-reliably-testing-async-pt1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## [Point-Free](https://www.pointfree.co)

> #### This directory contains code from Point-Free Episode: [Reliable Async Tests: The Problem](https://www.pointfree.co/episodes/ep238-reliable-async-tests)
>
> While Swift provides wonderful tools for writing async code, there are gaps in its tools for testing it. Let’s explore the tools it _does_ provide to show where they succeed, and where they fall short.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD7C2A30B827007675BD"
BuildableName = "ReliablyTestingAsync.app"
BlueprintName = "ReliablyTestingAsync"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:ReliablyTestingAsyncTests/ReliablyTestingAsync.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD8C2A30B829007675BD"
BuildableName = "ReliablyTestingAsyncTests.xctest"
BlueprintName = "ReliablyTestingAsyncTests"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD962A30B829007675BD"
BuildableName = "ReliablyTestingAsyncUITests.xctest"
BlueprintName = "ReliablyTestingAsyncUITests"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD7C2A30B827007675BD"
BuildableName = "ReliablyTestingAsync.app"
BlueprintName = "ReliablyTestingAsync"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD7C2A30B827007675BD"
BuildableName = "ReliablyTestingAsync.app"
BlueprintName = "ReliablyTestingAsync"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SwiftUI

@main
struct ReliablyTestingAsyncApp: App {
var body: some Scene {
WindowGroup {
ContentView(model: NumberFactModel())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Dependencies
import SwiftUI

struct NumberFactClient {
var fact: @Sendable (Int) async throws -> String
}

extension NumberFactClient: DependencyKey {
static let liveValue = Self { number in
try await Task.sleep(for: .seconds(1))
return try await String(
decoding: URLSession.shared.data(from: URL(string: "http://numbersapi.com/\(number)")!).0,
as: UTF8.self
)
}
}

extension DependencyValues {
var numberFact: NumberFactClient {
get { self[NumberFactClient.self] }
set { self[NumberFactClient.self] = newValue }
}
}

@MainActor
class NumberFactModel: ObservableObject {
@Dependency(\.numberFact) var numberFact

@Published var count = 0
@Published var fact: String?
@Published var isLoading = false

func incrementButtonTapped() {
self.fact = nil
self.count += 1
}
func decrementButtonTapped() {
self.fact = nil
self.count -= 1
}
func getFactButtonTapped() async {
self.isLoading = true
defer { self.isLoading = false }

self.fact = nil
do {
self.fact = try await self.numberFact.fact(self.count)
} catch {
// TODO: handle error
}
}
}

struct ContentView: View {
@ObservedObject var model: NumberFactModel

var body: some View {
Form {
Section {
HStack {
Button("-") { self.model.decrementButtonTapped() }
Text("\(self.model.count)")
Button("+") { self.model.incrementButtonTapped() }
}
}
.buttonStyle(.plain)

HStack {
Button("Get fact") {
Task { await self.model.getFactButtonTapped() }
}
if self.model.isLoading {
Spacer()
ProgressView()
}
}

if let fact = self.model.fact {
Text(fact)
}
}
}
}

struct ContentPreviews: PreviewProvider {
static var previews: some View {
ContentView(model: NumberFactModel())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

public final class LockIsolated<Value>: @unchecked Sendable {
private var _value: Value
private let lock = NSRecursiveLock()
public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
self._value = try value()
}
public func withValue<T: Sendable>(
_ operation: (inout Value) throws -> T
) rethrows -> T {
try self.lock.withLock {
var value = self._value
defer { self._value = value }
return try operation(&value)
}
}
}

extension LockIsolated where Value: Sendable {
public var value: Value {
self.lock.withLock {
self._value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Dependencies
import XCTest
@testable import ReliablyTestingAsync

@MainActor
final class NumberFactModelTests: XCTestCase {
func testIncrementDecrement() {
let model = NumberFactModel()
model.incrementButtonTapped()
XCTAssertEqual(model.count, 1)
model.decrementButtonTapped()
XCTAssertEqual(model.count, 0)
}

func testGetFact() async {
let model = withDependencies {
$0.numberFact.fact = { "\($0) is a good number." }
} operation: {
NumberFactModel()
}
await model.getFactButtonTapped()
XCTAssertEqual(model.fact, "0 is a good number.")

model.incrementButtonTapped()
XCTAssertEqual(model.fact, nil)

await model.getFactButtonTapped()
XCTAssertEqual(model.fact, "1 is a good number.")
}

func testFactClearsOut() async {
let fact = AsyncStream.makeStream(of: String.self)

let model = withDependencies {
$0.numberFact.fact = { _ in
await fact.stream.first(where: { _ in true })!
}
} operation: {
NumberFactModel()
}
model.fact = "An old fact about 0."

let task = Task { await model.getFactButtonTapped() }
await Task.yield()
XCTAssertEqual(model.fact, nil)
fact.continuation.yield("0 is a good number.")
await task.value
XCTAssertEqual(model.fact, "0 is a good number.")
}

func testFactIsLoading() async {
let fact = AsyncStream.makeStream(of: String.self)

let model = withDependencies {
$0.numberFact.fact = { _ in
await fact.stream.first(where: { _ in true })!
}
} operation: {
NumberFactModel()
}
model.fact = "An old fact about 0."

let task = Task { await model.getFactButtonTapped() }
await Task.yield()
XCTAssertEqual(model.isLoading, true)
fact.continuation.yield("0 is a good number.")
await task.value
XCTAssertEqual(model.fact, "0 is a good number.")
XCTAssertEqual(model.isLoading, false)
}
}
Loading

0 comments on commit 4b55f51

Please sign in to comment.