Swift_Testing
Swift Testing
Intro
New framework by apple which would be more expressive and is more aligned with what apple has started pushing for swift concurrency manifesto + macros in recent years.
For me it seems like a first party / class citizen treatment finally from apple to its testing framework tools.
Code
Renames
Test Throws
#expect(throws: URLParserPlayerError.self) {
try urlResolver.isTemplateValid(resolverInput)
}
#expect {
try urlResolver.isTemplateValid(resolverInput)
}
throws: { error in
if let errorParsed = error as? URLParserPlayerError {
#expect(errorParsed != nil)
#expect(errorDescription == errorParsed.description)
}
}
Problems
How to Wait
wait(for:timeout:)
Combine
Combine reactive paradigm testing with expectations to confirmation
@Test
func imageRetrieved() async {
var cancellable: AnyCancellable?
await confirmation { imageRetrieved in
cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
#expect($0 != nil)
imageRetrieved()
}
}
cancellable?.cancel()
}
Converted to use AsyncSequence on the publishers
@Test
func imageRetrieved() async {
let value = await viewModel.$image.values.first()
#expect(value != nil)
}
With this extension to simplify the call to first(where:):
extension AsyncSequence {
func first() async rethrows -> Element? {
try await first(where: { _ in true})
}
}
Parallelize
Default all the unit tests are parallel. You can make the unit test serialized or all the unit tests in a specific class serialized.
All unit test
@Suite(.serialized)
struct EntosAdBeaconTests {
@Test("test1")
  func test1() async { }
 Â
  @Test("test2")
  func test2() async { }
 Â
  @Test("test3")
  func test3() async { }
}
Specific unit test
@Test("test1", .serialized, arguments: [1, 3, 33, 69])
func testMultiple(values: Int) async { }
}
Struct
setupWithError() -> init()
teardown() -> deinit()
struct AdBeaconTests : ~Copyable {
init() async throws { }
deinit { }
}
Completion handler migration
donnywals | testing-completion-handler-apis-with-swift-testing
XCTest
Mock Class
final class MockAdBeaconable: AdBeaconable {
// define the expectation
let expectation: XCTestExpectation
var resultType: BeaconType? = nil
init(expectation: XCTestExpectation,
resultType: BeaconType? = nil) {
self.expectation = expectation
self.resultType = resultType
}
// Callback from protocol DI
func prepare(type: BeaconType) {
if type == resultType {
expectation.fulfill()
}
}
}
unit test
final class AdExpectationTests: XCTestCase {
func testExpectationOverload() {
let expectation = self.expectation(description: "Expect fulfillment from one eligible segment")
expectation.expectedFulfillmentCount = 1
// expectation.isInverted = true
let mockProvider = MockAdBeaconable(expectation: expectation)
let resolver = LinearAdResolver(adProvider: mockProvider)
// supply events
Task {
resolver.processSomeEvents(MockEvents.progress))
}
wait(for: [expectation], timeout: 3)
}
}
Swift Testing Continuation
Mock class conforming with DI protocol
final class MockAdBeaconable: AdBeaconable {
var resultType: BeaconType? = nil
// define the async continuation point
private var continuation: CheckedContinuation<BeaconType, Never>?
init(resultType: BeaconType? = nil) { }
// Hang point
func getBeacons() async -> BeaconType {
await withCheckedContinuation { (continuation: CheckedContinuation<BeaconType, Never>) in
self.continuation = continuation
}
}
// Callback from protocol DI
func prepare(type: BeaconType) {
if type == resultType {
continuation?.resume(returning: type)
continuation = nil
}
}
}
Unit test trying to utilize dependency injection, instead of using Wait for expectations, utilizing Continuation
@Test("Expectations fulfill to Continuation")
func waitExpectationWithContinuation() async {
// Initialize Dependency Injection wrapper
let mockBeaconer = MockAdBeaconable(resultType: .adBreakStart)
let adBeaconingManager = AdBeaconing(handler: mockBeaconer)
// Initialize Event Bus
let mockAdPlayerEventBus = MockAdBus(bus: mockEventBus)
adBeaconingManager.configure(with: mockAdPlayerEventBus)
// Send mock events
mockAdPlayerEventBus.bus.send(mockEvent1)
mockAdPlayerEventBus.bus.send(mockEvent2)
// Wait for async blocking call
let result = await mockBeaconer.getBeacons()
// Assert / check
#expect(result == .adBreakStart)
}
XCTest Time wait
var executionTimeAllowance: TimeInterval { get set }
old
final class DataHandlingTests: XCTestCase {
func test_loadNames() async {
let expectation = XCTestExpectation(description: "long task")
await fulfillment(of: [expectation], timeout: 60)
}
}
final class DataHandlingTests: XCTestCase {
override setUp() {
self.executionTimeAllowance = 60
}
}
new
struct DataHandlingTests
@Test("loading", .timeLimit(.minutes(1)))
func test_loadNames() { }