Skip to content

Commit

Permalink
Merge pull request #61 from ably-labs/55-switch-to-swift-testing
Browse files Browse the repository at this point in the history
[ECO-4990] Switch to Swift Testing
  • Loading branch information
lawrence-forooghian authored Sep 23, 2024
2 parents 7ef0a0e + f5cab27 commit 90b9ac1
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 114 deletions.
18 changes: 10 additions & 8 deletions Tests/AblyChatTests/DefaultChatClientTests.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
@testable import AblyChat
import XCTest
import Testing

class DefaultChatClientTests: XCTestCase {
func test_init_withoutClientOptions() {
struct DefaultChatClientTests {
@Test
func init_withoutClientOptions() {
// Given: An instance of DefaultChatClient is created with nil clientOptions
let client = DefaultChatClient(realtime: MockRealtime.create(), clientOptions: nil)

// Then: It uses the default client options
let defaultOptions = ClientOptions()
XCTAssertTrue(client.clientOptions.isEqualForTestPurposes(defaultOptions))
#expect(client.clientOptions.isEqualForTestPurposes(defaultOptions))
}

func test_rooms() throws {
@Test
func rooms() throws {
// Given: An instance of DefaultChatClient
let realtime = MockRealtime.create()
let options = ClientOptions()
Expand All @@ -20,8 +22,8 @@ class DefaultChatClientTests: XCTestCase {
// Then: Its `rooms` property returns an instance of DefaultRooms with the same realtime client and client options
let rooms = client.rooms

let defaultRooms = try XCTUnwrap(rooms as? DefaultRooms)
XCTAssertIdentical(defaultRooms.realtime, realtime)
XCTAssertTrue(defaultRooms.clientOptions.isEqualForTestPurposes(options))
let defaultRooms = try #require(rooms as? DefaultRooms)
#expect(defaultRooms.realtime === realtime)
#expect(defaultRooms.clientOptions.isEqualForTestPurposes(options))
}
}
27 changes: 15 additions & 12 deletions Tests/AblyChatTests/DefaultInternalLoggerTests.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
@testable import AblyChat
import XCTest
import Testing

class DefaultInternalLoggerTests: XCTestCase {
func test_defaults() {
struct DefaultInternalLoggerTests {
@Test
func defaults() {
let logger = DefaultInternalLogger(logHandler: nil, logLevel: nil)

XCTAssertTrue(logger.logHandler is DefaultLogHandler)
XCTAssertEqual(logger.logLevel, .error)
#expect(logger.logHandler is DefaultLogHandler)
#expect(logger.logLevel == .error)
}

func test_log() throws {
@Test
func log() throws {
// Given: A DefaultInternalLogger instance
let logHandler = MockLogHandler()
let logger = DefaultInternalLogger(logHandler: logHandler, logLevel: nil)
Expand All @@ -22,13 +24,14 @@ class DefaultInternalLoggerTests: XCTestCase {
)

// Then: It calls log(…) on the underlying logger, interpolating the code location into the message and passing through the level
let logArguments = try XCTUnwrap(logHandler.logArguments)
XCTAssertEqual(logArguments.message, "(Ably/Room.swift:123) Hello")
XCTAssertEqual(logArguments.level, .error)
XCTAssertNil(logArguments.context)
let logArguments = try #require(logHandler.logArguments)
#expect(logArguments.message == "(Ably/Room.swift:123) Hello")
#expect(logArguments.level == .error)
#expect(logArguments.context == nil)
}

func test_log_whenLogLevelArgumentIsLessSevereThanLogLevelProperty_itDoesNotLog() {
@Test
func log_whenLogLevelArgumentIsLessSevereThanLogLevelProperty_itDoesNotLog() {
// Given: A DefaultInternalLogger instance
let logHandler = MockLogHandler()
let logger = DefaultInternalLogger(
Expand All @@ -44,6 +47,6 @@ class DefaultInternalLoggerTests: XCTestCase {
)

// Then: It does not call `log(…)` on the underlying logger
XCTAssertNil(logHandler.logArguments)
#expect(logHandler.logArguments == nil)
}
}
33 changes: 14 additions & 19 deletions Tests/AblyChatTests/DefaultRoomStatusTests.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
@testable import AblyChat
import XCTest
import Testing

class DefaultRoomStatusTests: XCTestCase {
func test_current_startsAsInitialized() async {
struct DefaultRoomStatusTests {
@Test
func current_startsAsInitialized() async {
let status = DefaultRoomStatus(logger: TestLogger())
let current = await status.current
XCTAssertEqual(current, .initialized)
#expect(await status.current == .initialized)
}

func test_error_startsAsNil() async {
@Test()
func error_startsAsNil() async {
let status = DefaultRoomStatus(logger: TestLogger())
let error = await status.error
XCTAssertNil(error)
#expect(await status.error == nil)
}

func test_transition() async {
@Test
func transition() async throws {
// Given: A RoomStatus
let status = DefaultRoomStatus(logger: TestLogger())
let originalState = await status.current
Expand All @@ -30,17 +31,11 @@ class DefaultRoomStatusTests: XCTestCase {
await status.transition(to: newState)

// Then: It emits a status change to all subscribers added via onChange(bufferingPolicy:), and updates its `current` property to the new state
guard let statusChange1 = await statusChange1, let statusChange2 = await statusChange2 else {
XCTFail("Expected status changes to be emitted")
return
for statusChange in try await [#require(statusChange1), #require(statusChange2)] {
#expect(statusChange.previous == originalState)
#expect(statusChange.current == newState)
}

for statusChange in [statusChange1, statusChange2] {
XCTAssertEqual(statusChange.previous, originalState)
XCTAssertEqual(statusChange.current, newState)
}

let current = await status.current
XCTAssertEqual(current, .attached)
#expect(await status.current == .attached)
}
}
44 changes: 18 additions & 26 deletions Tests/AblyChatTests/DefaultRoomTests.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Ably
@testable import AblyChat
import XCTest
import Testing

class DefaultRoomTests: XCTestCase {
func test_attach_attachesAllChannels_andSucceedsIfAllSucceed() async throws {
struct DefaultRoomTests {
@Test
func attach_attachesAllChannels_andSucceedsIfAllSucceed() async throws {
// Given: a DefaultRoom instance with ID "basketball", with a Realtime client for which `attach(_:)` completes successfully if called on the following channels:
//
// - basketball::$chat::$chatMessages
Expand All @@ -26,19 +27,15 @@ class DefaultRoomTests: XCTestCase {

// Then: `attach(_:)` is called on each of the channels, the room `attach` call succeeds, and the room transitions to ATTACHED
for channel in channelsList {
XCTAssertTrue(channel.attachCallCounter.isNonZero)
#expect(channel.attachCallCounter.isNonZero)
}

guard let attachedStatusChange = await attachedStatusChange else {
XCTFail("Expected status change to ATTACHED but didn't get one")
return
}
let currentStatus = await room.status.current
XCTAssertEqual(currentStatus, .attached)
XCTAssertEqual(attachedStatusChange.current, .attached)
#expect(await room.status.current == .attached)
#expect(try #require(await attachedStatusChange).current == .attached)
}

func test_attach_attachesAllChannels_andFailsIfOneFails() async throws {
@Test
func attach_attachesAllChannels_andFailsIfOneFails() async throws {
// Given: a DefaultRoom instance, with a Realtime client for which `attach(_:)` completes successfully if called on the following channels:
//
// - basketball::$chat::$chatMessages
Expand All @@ -65,11 +62,11 @@ class DefaultRoomTests: XCTestCase {
}

// Then: the room `attach` call fails with the same error as the channel `attach(_:)` call
let roomAttachErrorInfo = try XCTUnwrap(roomAttachError as? ARTErrorInfo)
XCTAssertIdentical(roomAttachErrorInfo, channelAttachError)
#expect(try #require(roomAttachError as? ARTErrorInfo) === channelAttachError)
}

func test_detach_detachesAllChannels_andSucceedsIfAllSucceed() async throws {
@Test
func detach_detachesAllChannels_andSucceedsIfAllSucceed() async throws {
// Given: a DefaultRoom instance with ID "basketball", with a Realtime client for which `detach(_:)` completes successfully if called on the following channels:
//
// - basketball::$chat::$chatMessages
Expand All @@ -92,19 +89,15 @@ class DefaultRoomTests: XCTestCase {

// Then: `detach(_:)` is called on each of the channels, the room `detach` call succeeds, and the room transitions to DETACHED
for channel in channelsList {
XCTAssertTrue(channel.detachCallCounter.isNonZero)
#expect(channel.detachCallCounter.isNonZero)
}

guard let detachedStatusChange = await detachedStatusChange else {
XCTFail("Expected status change to DETACHED but didn't get one")
return
}
let currentStatus = await room.status.current
XCTAssertEqual(currentStatus, .detached)
XCTAssertEqual(detachedStatusChange.current, .detached)
#expect(await room.status.current == .detached)
#expect(try #require(await detachedStatusChange).current == .detached)
}

func test_detach_detachesAllChannels_andFailsIfOneFails() async throws {
@Test
func detach_detachesAllChannels_andFailsIfOneFails() async throws {
// Given: a DefaultRoom instance, with a Realtime client for which `detach(_:)` completes successfully if called on the following channels:
//
// - basketball::$chat::$chatMessages
Expand All @@ -131,7 +124,6 @@ class DefaultRoomTests: XCTestCase {
}

// Then: the room `detach` call fails with the same error as the channel `detach(_:)` call
let roomDetachErrorInfo = try XCTUnwrap(roomDetachError as? ARTErrorInfo)
XCTAssertIdentical(roomDetachErrorInfo, channelDetachError)
#expect(try #require(roomDetachError as? ARTErrorInfo) === channelDetachError)
}
}
25 changes: 14 additions & 11 deletions Tests/AblyChatTests/DefaultRoomsTests.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
@testable import AblyChat
import XCTest
import Testing

class DefaultRoomsTests: XCTestCase {
struct DefaultRoomsTests {
// @spec CHA-RC1a
func test_get_returnsRoomWithGivenID() async throws {
@Test
func get_returnsRoomWithGivenID() async throws {
// Given: an instance of DefaultRooms
let realtime = MockRealtime.create()
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger())
Expand All @@ -14,14 +15,15 @@ class DefaultRoomsTests: XCTestCase {
let room = try await rooms.get(roomID: roomID, options: options)

// Then: It returns a DefaultRoom instance that uses the same Realtime instance, with the given ID and options
let defaultRoom = try XCTUnwrap(room as? DefaultRoom)
XCTAssertIdentical(defaultRoom.realtime, realtime)
XCTAssertEqual(defaultRoom.roomID, roomID)
XCTAssertEqual(defaultRoom.options, options)
let defaultRoom = try #require(room as? DefaultRoom)
#expect(defaultRoom.realtime === realtime)
#expect(defaultRoom.roomID == roomID)
#expect(defaultRoom.options == options)
}

// @spec CHA-RC1b
func test_get_returnsExistingRoomWithGivenID() async throws {
@Test
func get_returnsExistingRoomWithGivenID() async throws {
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID
let realtime = MockRealtime.create()
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger())
Expand All @@ -34,11 +36,12 @@ class DefaultRoomsTests: XCTestCase {
let secondRoom = try await rooms.get(roomID: roomID, options: options)

// Then: It returns the same room object
XCTAssertIdentical(secondRoom, firstRoom)
#expect(secondRoom === firstRoom)
}

// @spec CHA-RC1c
func test_get_throwsErrorWhenOptionsDoNotMatch() async throws {
@Test
func get_throwsErrorWhenOptionsDoNotMatch() async throws {
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID and options
let realtime = MockRealtime.create()
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger())
Expand All @@ -59,6 +62,6 @@ class DefaultRoomsTests: XCTestCase {
}

// Then: It throws an inconsistentRoomOptions error
try assertIsChatError(caughtError, withCode: .inconsistentRoomOptions)
#expect(isChatError(caughtError, withCode: .inconsistentRoomOptions))
}
}
16 changes: 8 additions & 8 deletions Tests/AblyChatTests/Helpers/Helpers.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import Ably
@testable import AblyChat
import XCTest

/**
Asserts that a given optional `Error` is an `ARTErrorInfo` in the chat error domain with a given code.
Tests whether a given optional `Error` is an `ARTErrorInfo` in the chat error domain with a given code.
*/
func assertIsChatError(_ maybeError: (any Error)?, withCode code: AblyChat.ErrorCode, file: StaticString = #filePath, line: UInt = #line) throws {
let error = try XCTUnwrap(maybeError, "Expected an error", file: file, line: line)
let ablyError = try XCTUnwrap(error as? ARTErrorInfo, "Expected an ARTErrorInfo", file: file, line: line)
func isChatError(_ maybeError: (any Error)?, withCode code: AblyChat.ErrorCode) -> Bool {
guard let ablyError = maybeError as? ARTErrorInfo else {
return false
}

XCTAssertEqual(ablyError.domain, AblyChat.errorDomain as String, file: file, line: line)
XCTAssertEqual(ablyError.code, code.rawValue, file: file, line: line)
XCTAssertEqual(ablyError.statusCode, code.statusCode, file: file, line: line)
return ablyError.domain == AblyChat.errorDomain as String
&& ablyError.code == code.rawValue
&& ablyError.statusCode == code.statusCode
}
15 changes: 8 additions & 7 deletions Tests/AblyChatTests/InternalLoggerTests.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
@testable import AblyChat
import XCTest
import Testing

class InternalLoggerTests: XCTestCase {
func test_protocolExtension_logMessage_defaultArguments_populatesFileIDAndLine() throws {
struct InternalLoggerTests {
@Test
func protocolExtension_logMessage_defaultArguments_populatesFileIDAndLine() throws {
let logger = MockInternalLogger()

let expectedLine = #line + 1
logger.log(message: "Here is a message", level: .info)

let receivedArguments = try XCTUnwrap(logger.logArguments)
let receivedArguments = try #require(logger.logArguments)

XCTAssertEqual(receivedArguments.level, .info)
XCTAssertEqual(receivedArguments.message, "Here is a message")
XCTAssertEqual(receivedArguments.codeLocation, .init(fileID: #fileID, line: expectedLine))
#expect(receivedArguments.level == .info)
#expect(receivedArguments.message == "Here is a message")
#expect(receivedArguments.codeLocation == .init(fileID: #fileID, line: expectedLine))
}
}
25 changes: 12 additions & 13 deletions Tests/AblyChatTests/MessageSubscriptionTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@testable import AblyChat
import AsyncAlgorithms
import XCTest
import Testing

private final class MockPaginatedResult<T>: PaginatedResult {
var items: [T] { fatalError("Not implemented") }
Expand All @@ -18,39 +18,38 @@ private final class MockPaginatedResult<T>: PaginatedResult {
init() {}
}

class MessageSubscriptionTests: XCTestCase {
struct MessageSubscriptionTests {
let messages = ["First", "Second"].map { text in
Message(timeserial: "", clientID: "", roomID: "", text: text, createdAt: .init(), metadata: [:], headers: [:])
}

func testWithMockAsyncSequence() async {
@Test
func withMockAsyncSequence() async {
let subscription = MessageSubscription(mockAsyncSequence: messages.async) { _ in fatalError("Not implemented") }

async let emittedElements = Array(subscription.prefix(2))

let awaitedEmittedElements = await emittedElements
XCTAssertEqual(awaitedEmittedElements.map(\.text), ["First", "Second"])
#expect(await Array(subscription.prefix(2)).map(\.text) == ["First", "Second"])
}

func testEmit() async {
@Test
func emit() async {
let subscription = MessageSubscription(bufferingPolicy: .unbounded)

async let emittedElements = Array(subscription.prefix(2))

subscription.emit(messages[0])
subscription.emit(messages[1])

let awaitedEmittedElements = await emittedElements
XCTAssertEqual(awaitedEmittedElements.map(\.text), ["First", "Second"])
#expect(await emittedElements.map(\.text) == ["First", "Second"])
}

func testMockGetPreviousMessages() async throws {
@Test
func mockGetPreviousMessages() async throws {
let mockPaginatedResult = MockPaginatedResult<Message>()
let subscription = MessageSubscription(mockAsyncSequence: [].async) { _ in mockPaginatedResult }

let result = try await subscription.getPreviousMessages(params: .init())
// This dance is to avoid the compiler error "Runtime support for parameterized protocol types is only available in iOS 16.0.0 or newer" — casting back to a concrete type seems to avoid this
let resultAsConcreteType = try XCTUnwrap(result as? MockPaginatedResult<Message>)
XCTAssertIdentical(resultAsConcreteType, mockPaginatedResult)
let resultAsConcreteType = try #require(result as? MockPaginatedResult<Message>)
#expect(resultAsConcreteType === mockPaginatedResult)
}
}
Loading

0 comments on commit 90b9ac1

Please sign in to comment.