Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Notes on testing concurrent code in swift
- Loading branch information
Showing
4 changed files
with
157 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Concurrency | ||
|
||
Async/Await, Tasks, Actors and Sendable in Swift. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# Testing Concurrent Code | ||
|
||
There's enough differences between testing code using Swift Concurrency (async/await) and testing more traditional, callback-based asynchronous code. This page intends to document some of what I've learned. | ||
|
||
## Make Fakes Threadsafe | ||
|
||
Because Swift Concurrency will run your code concurrently, you are at much higher risk of running into concurrent access problems. Especially if you use something like Nimble's `toEventually` behavior. Which you almost have to use if you want to be able to test the in progress state of concurrent code. | ||
|
||
I've found that, where possible, making fakes be Actors is the best way to handle this. Actors implicitly conform to Sendable, and they handle concurrent access by only allowing one thread of work to access it at a time. However, you can also make fakes threadsafe by using tools like locks. If you don't do this, then you'll run into annoying common and hard to diagnose test crashes. | ||
|
||
## Pre-resolve Concurrent Fakes | ||
|
||
One of the assumptions in Swift Concurrency is that threads will never be blocked. Which was a problem because my preferred approach for testing asynchronous code essentially blocks the running thread of work until the test resolves it (and sometimes the test only ever tests the in-progress state, so doesn't need to resolve). This end up resulting in incredibly high test flakiness until I realized it's better to pre-resolve concurrent functions - or, basically, treat them like they're synchronous functions. | ||
|
||
For example, consider this given bit of code: | ||
|
||
```swift | ||
protocol SomeDependencyProtocol { | ||
func method(_ callback: @escaping (Int) -> Void) | ||
} | ||
|
||
struct SubjectUnderTest { | ||
let dependency: SomeDependencyProtocol | ||
|
||
func perform(_ callback: @escaping (String) -> Void) { | ||
dependency.method { | ||
callback(String(describing: $0)) | ||
} | ||
} | ||
} | ||
|
||
// Sample Fake Implementation. | ||
// For brevity, ignoring thread-safety concerns. | ||
final class FakeSomeDependencyProtocol { | ||
private(set) var methodCalls = [(Int) -> Void]() | ||
func method(_ callback: @escaping (Int) -> Void) { | ||
methodCalls.append(callback) | ||
} | ||
} | ||
``` | ||
|
||
Traditionally, I would test it almost in an Act Arrange Assert matter. | ||
|
||
```swift | ||
var subject: SubjectUnderTest! | ||
var dependency: FakeSomeDependencyProtocol! | ||
|
||
describe("perform(_:)") { | ||
var result: String? = nil | ||
beforeEach { | ||
result = nil | ||
|
||
subject.perform { | ||
result = $0 | ||
} | ||
} | ||
|
||
// ... | ||
|
||
describe("when the dependency returns") { | ||
beforeEach { | ||
dependency.methodCalls.last?(1) | ||
} | ||
|
||
it("calls the callback with the stringified value") { | ||
expect(result).to(equal("1")) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Instead, I learned that it's significantly more reliable to pre-resolve the async dependencies, like so, like so: | ||
|
||
```swift | ||
protocol SomeDependencyProtocol { | ||
func method() async -> Int | ||
} | ||
|
||
struct SubjectUnderTest { | ||
let dependency: SomeDependencyProtocol | ||
|
||
func perform() async -> String { | ||
String(describing: await dependency.method()) | ||
} | ||
} | ||
|
||
// Sample Fake Implementation. | ||
actor FakeSomeDependencyProtocol { | ||
var methodStub = 0 | ||
func setMethodStub(_ stub: Int) { | ||
methodStub = stub | ||
} | ||
func method() async -> Int { | ||
methodStub | ||
} | ||
} | ||
``` | ||
|
||
With a test, using `justBeforeEach` to allow the same structure as before: | ||
|
||
```swift | ||
var subject: SubjectUnderTest! | ||
var dependency: FakeSomeDependencyProtocol! | ||
|
||
describe("perform(_:)") { | ||
var task: Task<String, Never>? | ||
justBeforeEach { | ||
task = Task { [subject] in await subject!.perform() } | ||
} | ||
|
||
afterEach { | ||
task?.cancel() | ||
task = nil | ||
} | ||
|
||
// ... | ||
|
||
describe("when the dependency returns") { | ||
beforeEach { | ||
await dependency.setMethodStub(1) | ||
} | ||
|
||
it("returns the stringified value") { | ||
await expect { await task!.value }.to(equal("1")) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
For the unfamiliar, in Quick, using `justBeforeEach` means the passed-in closure will run after the other `beforeEach` closures in a test. So, in this example, the `await dependency.setMethodStub(1)` line will run before the `task = Task { ... }` line. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# To Don't | ||
|
||
The opposite of a ToDo list. | ||
|
||
This is mostly lists of projects I am interested in starting, but I know I will be a complete waste of time if I ever start it. These definitely could work with a team of people, but for a solo project, these aren't worth it. | ||
|
||
- Web Browser. Something maybe like [Midori](https://en.wikipedia.org/wiki/Midori_(web_browser)) was a number of years ago (I last used it ~2010). Frankly, outside of basic stuff you can build on top of `WKWebView`, browsers aren't worth it. There's an (unfortunate) reason that there are really only 3 browsers out there. | ||
- Email client. I actually built one ages ago when I was still in high school. Learned how to use POP and SMTP from that. But with more modern requirements, email clients aren't worth my time. |