Testing & mocking
Generating mocks
Testing generated Connect-Swift APIs is easily achieved
by using the connect-swift-mocks plugin
to generate mock client implementations from your Protobuf
definitions. This plugin supports all of the same
options that the
production connect-swift plugin supports.
This buf.gen.yaml file demonstrates generating production
interfaces and implementations into the Generated folder, and a corresponding
set of mocks into the GeneratedMocks folder:
version: v1
plugins:
  # Generated models
  - plugin: buf.build/apple/swift
    opt:
      - Visibility=Public
    out: Generated
  # Production generated services/methods
  - plugin: buf.build/connectrpc/swift
    opt:
      - GenerateAsyncMethods=true
      - GenerateCallbackMethods=true
      - Visibility=Public
    out: Generated
  # Mock generated services/methods
  - plugin: buf.build/connectrpc/swift-mocks
    opt:
      - GenerateAsyncMethods=true
      - GenerateCallbackMethods=true
      - Visibility=Public
    out: GeneratedMocks
The GenerateAsyncMethods and
GenerateCallbackMethods options
that you specify must match the option(s) you're using for production
clients.
As an example, consider this Protobuf file:
syntax = "proto3";
package connectrpc.eliza.v1;
service ElizaService {
    rpc Say(SayRequest) returns (SayResponse) {}
    rpc Converse(stream ConverseRequest) returns (stream ConverseResponse) {}
}
message SayRequest {
    string sentence = 1;
}
message SayResponse {
    string sentence = 1;
}
message ConverseRequest {
    string sentence = 1;
}
message ConverseResponse {
    string sentence = 1;
}
When the production connect-swift plugin is invoked, it outputs
2 things for each service:
- A protocol interface ending with 
*ClientInterface - A production implementation that conforms to the protocol and ends with 
*Client 
Click to expand eliza.connect.swift
import Connect
import Foundation
import SwiftProtobuf
public protocol Connectrpc_Eliza_V1_ElizaServiceClientInterface: Sendable {
    @discardableResult
    func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Headers, completion: @escaping @Sendable (ResponseMessage<Connectrpc_Eliza_V1_SayResponse>) -> Void) -> Cancelable
    func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Headers) async -> ResponseMessage<Connectrpc_Eliza_V1_SayResponse>
    func `converse`(headers: Headers, onResult: @escaping @Sendable (StreamResult<Connectrpc_Eliza_V1_ConverseResponse>) -> Void) -> any BidirectionalStreamInterface<Connectrpc_Eliza_V1_ConverseRequest>
    func `converse`(headers: Headers) -> any BidirectionalAsyncStreamInterface<Connectrpc_Eliza_V1_ConverseRequest, Connectrpc_Eliza_V1_ConverseResponse>
}
/// Concrete implementation of `Connectrpc_Eliza_V1_ElizaServiceClientInterface`.
public final class Connectrpc_Eliza_V1_ElizaServiceClient: Connectrpc_Eliza_V1_ElizaServiceClientInterface, Sendable {
    private let client: ProtocolClientInterface
    public init(client: ProtocolClientInterface) {
        self.client = client
    }
    @discardableResult
    public func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Headers = [:], completion: @escaping @Sendable (ResponseMessage<Connectrpc_Eliza_V1_SayResponse>) -> Void) -> Cancelable {
        return self.client.unary(path: "connectrpc.eliza.v1.ElizaService/Say", request: request, headers: headers, completion: completion)
    }
    public func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Headers = [:]) async -> ResponseMessage<Connectrpc_Eliza_V1_SayResponse> {
        return await self.client.unary(path: "connectrpc.eliza.v1.ElizaService/Say", request: request, headers: headers)
    }
    public func `converse`(headers: Headers = [:], onResult: @escaping @Sendable (StreamResult<Connectrpc_Eliza_V1_ConverseResponse>) -> Void) -> any BidirectionalStreamInterface<Connectrpc_Eliza_V1_ConverseRequest> {
        return self.client.bidirectionalStream(path: "connectrpc.eliza.v1.ElizaService/Converse", headers: headers, onResult: onResult)
    }
    public func `converse`(headers: Headers = [:]) -> any BidirectionalAsyncStreamInterface<Connectrpc_Eliza_V1_ConverseRequest, Connectrpc_Eliza_V1_ConverseResponse> {
        return self.client.bidirectionalStream(path: "connectrpc.eliza.v1.ElizaService/Converse", headers: headers)
    }
}
When the mock connect-swift-mocks plugin is invoked, it outputs a
.mock.swift file which includes an implementation ending with *ClientMock
that conforms to the same interface as the production client:
Click to expand eliza.mock.swift
import Combine
import Connect
import ConnectMocks
import Foundation
import SwiftProtobuf
/// Mock implementation of `Connectrpc_Eliza_V1_ElizaServiceClientInterface`.
open class Connectrpc_Eliza_V1_ElizaServiceClientMock: Connectrpc_Eliza_V1_ElizaServiceClientInterface, @unchecked Sendable {
    private var cancellables = [Combine.AnyCancellable]()
    /// Mocked for calls to `say()`.
    public var mockSay = { (_: Connectrpc_Eliza_V1_SayRequest) -> ResponseMessage<Connectrpc_Eliza_V1_SayResponse> in .init(result: .success(.init())) }
    /// Mocked for async calls to `say()`.
    public var mockAsyncSay = { (_: Connectrpc_Eliza_V1_SayRequest) -> ResponseMessage<Connectrpc_Eliza_V1_SayResponse> in .init(result: .success(.init())) }
    /// Mocked for calls to `converse()`.
    public var mockConverse = MockBidirectionalStream<Connectrpc_Eliza_V1_ConverseRequest, Connectrpc_Eliza_V1_ConverseResponse>()
    /// Mocked for async calls to `converse()`.
    public var mockAsyncConverse = MockBidirectionalAsyncStream<Connectrpc_Eliza_V1_ConverseRequest, Connectrpc_Eliza_V1_ConverseResponse>()
    public init() {}
    @discardableResult
    open func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Headers = [:], completion: @escaping @Sendable (ResponseMessage<Connectrpc_Eliza_V1_SayResponse>) -> Void) -> Cancelable {
        completion(self.mockSay(request))
        return Cancelable {}
    }
    open func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Headers = [:]) async -> ResponseMessage<Connectrpc_Eliza_V1_SayResponse> {
        return self.mockAsyncSay(request)
    }
    open func `converse`(headers: Headers = [:], onResult: @escaping @Sendable (StreamResult<Connectrpc_Eliza_V1_ConverseResponse>) -> Void) -> any BidirectionalStreamInterface<Connectrpc_Eliza_V1_ConverseRequest> {
        self.mockConverse.$inputs.first { !$0.isEmpty }.sink { _ in self.mockConverse.outputs.forEach(onResult) }.store(in: &self.cancellables)
        return self.mockConverse
    }
    open func `converse`(headers: Headers = [:]) -> any BidirectionalAsyncStreamInterface<Connectrpc_Eliza_V1_ConverseRequest, Connectrpc_Eliza_V1_ConverseResponse> {
        return self.mockAsyncConverse
    }
}
Using generated mocks
As mentioned in the tutorial, we recommend
having your application consume the *ClientInterface protocols rather than
the concrete types directly. Doing so allows for replacing the concrete
implementations with the generated mock implementations:
final class MessagingViewModel: ObservableObject {
    private let elizaClient: Connectrpc_Eliza_V1_ElizaServiceClientInterface
    init(elizaClient: Connectrpc_Eliza_V1_ElizaServiceClientInterface) {
        self.elizaClient = elizaClient
    }
    @Published private(set) var messages: [Message] {...}
    func send(_ sentence: String) async {
        let request = Connectrpc_Eliza_V1_SayRequest.with { $0.sentence = sentence }
        let response = await self.elizaClient.say(request: request, headers: [:])
        ...
    }
}
To use the generated mocks, you will need to include the
ConnectMocks library which is available in the
Connect-Swift repo alongside the Connect library.
It can be integrated via either:
- Swift Package Manager, using the same GitHub URL
and instructions as the
main 
Connectlibrary. - CocoaPods, using the 
Connect-Swift-MocksCocoaPod. 
You can then write unit tests that inject the mock implementations instead of the production implementations, making validating requests and providing mocked response data easy:
import Connect
import ConnectMocks
@testable import ElizaApp // The target containing your application logic
import SwiftProtobuf
import XCTest
final class ElizaAppTests: XCTestCase {
    /// Example test that injects a mock generated client into a unary view model.
    @MainActor
    func testUnaryMessagingViewModel() async {
        let client = Connectrpc_Eliza_V1_ElizaServiceClientMock()
        client.mockAsyncSay = { request in
            XCTAssertEqual(request.sentence, "hello!")
            return ResponseMessage(result: .success(.with { $0.sentence = "hi, i'm eliza!" }))
        }
        let viewModel = MessagingViewModel(elizaClient: client)
        await viewModel.send("hello!")
        XCTAssertEqual(viewModel.messages.count, 2)
        XCTAssertEqual(viewModel.messages[0].message, "hello!")
        XCTAssertEqual(viewModel.messages[0].author, .user)
        XCTAssertEqual(viewModel.messages[1].message, "hi, i'm eliza!")
        XCTAssertEqual(viewModel.messages[1].author, .eliza)
    }
}
Similar tests can be written for streaming, assuming a
BidirectionalStreamingMessagingViewModel that uses the generated async
version of the converse() streaming method:
/// Example test that injects a mock generated client into a bidirectional stream view model.
@MainActor
func testBidirectionalStreamMessagingViewModel() async {
    let client = Connectrpc_Eliza_V1_ElizaServiceClientMock()
    client.mockAsyncConverse.outputs = [.message(.with { $0.sentence = "hi, i'm eliza!" })]
    let viewModel = BidirectionalStreamingMessagingViewModel(elizaClient: client)
    await viewModel.send("hello!")
    await viewModel.send("hello again!")
    XCTAssertEqual(viewModel.messages[0].message, "hello!")
    XCTAssertEqual(viewModel.messages[0].author, .user)
    XCTAssertEqual(viewModel.messages[1].message, "hi, i'm eliza!")
    XCTAssertEqual(viewModel.messages[1].author, .eliza)
    XCTAssertEqual(viewModel.messages[2].message, "hello again!")
    XCTAssertEqual(viewModel.messages[2].author, .user)
}
Testing with @Sendable closures
If your codebase is not yet using async/await and is instead consuming
generated clients that provide completion/result closures which are annotated
with @Sendable, writing tests can prove challenging. For example:
func testGetUser() {
    let client = Users_V1_UsersMock()
    client.mockGetUserInfo = { request in
        return ResponseMessage(result: .success(...))
    }
    var receivedMessage: Users_V1_UserInfoResponse?
    client.getUserInfo(request: Users_V1_UserInfoRequest()) { response in
        // ERROR: Mutation of captured var 'receivedMessage' in concurrently-executing code
        receivedMessage = response.message
    }
    XCTAssertEqual(receivedMessage?.name, "jane")
}
One workaround for this is to wrap the captured type with a class
that conforms to Sendable. For example:
public final class Locked<T>: @unchecked Sendable {
    private let lock = NSLock()
    private var wrappedValue: T
    /// Thread-safe access to the underlying value.
    public var value: T {
        get {
            self.lock.lock()
            defer { self.lock.unlock() }
            return self.wrappedValue
        }
        set {
            self.lock.lock()
            self.wrappedValue = newValue
            self.lock.unlock()
        }
    }
    public init(_ value: T) {
        self.wrappedValue = value
    }
}
The above error can be solved by updating the test to use this wrapper:
func testGetUser() {
    let client = Users_V1_UsersMock()
    client.mockGetUserInfo = { request in
        return ResponseMessage(result: .success(...))
    }
    let receivedMessage = Locked<Users_V1_UserInfoResponse?>(nil)
    client.getUserInfo(request: Users_V1_UserInfoRequest()) { response in
        receivedMessage.value = response.message
    }
    XCTAssertEqual(receivedMessage.value?.name, "jane")
}