Guidelines and templates for writing effective unit tests with XCTest, including test-driven development practices and mocking techniques.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: Swift Unit Testing Skill description: Guidelines and templates for writing effective unit tests with XCTest, including test-driven development practices and mocking techniques. version: 1.0 activation: Activate for queries on Swift unit testing, XCTest framework, test-driven development, mocking, or testing best practices.
Swift Unit Testing Skill
This skill provides comprehensive guidance on writing effective unit tests using Apple's XCTest framework. It covers test-driven development (TDD), testing patterns, mocking strategies, and best practices for maintaining high-quality test suites.
Best Practices
-
Test First: Follow TDD principles - write tests before implementation.
-
Single Responsibility: Each test should verify one specific behavior.
-
Arrange-Act-Assert: Structure tests clearly with setup, execution, and verification phases.
-
Descriptive Names: Use descriptive test method names that explain what is being tested.
-
Independent Tests: Tests should not depend on each other or shared state.
-
Fast Execution: Keep tests fast to encourage frequent running.
Unit Testing Guidelines
- Test public interfaces, not implementation details.
- Use mocking for external dependencies.
- Cover both happy path and edge cases.
- Include tests for error conditions.
- Maintain test code quality equal to production code.
- Run tests frequently during development.
Examples
Example 1: Basic XCTest Structure
User Prompt: "Create a basic unit test class with setup and teardown methods."
Expected Output:
import XCTest
@testable import MyApp
class CalculatorTests: XCTestCase {
var calculator: Calculator!
override func setUp() {
super.setUp()
calculator = Calculator()
}
override func tearDown() {
calculator = nil
super.tearDown()
}
func testAddition() {
// Given
let a = 5
let b = 3
// When
let result = calculator.add(a, b)
// Then
XCTAssertEqual(result, 8, "Addition should return the sum of two numbers")
}
func testAdditionWithNegativeNumbers() {
// Given
let a = 5
let b = -3
// When
let result = calculator.add(a, b)
// Then
XCTAssertEqual(result, 2, "Addition should work with negative numbers")
}
func testDivisionByZero() {
// Given
let a = 10
let b = 0
// When & Then
XCTAssertThrowsError(try calculator.divide(a, b)) { error in
XCTAssertEqual(error as? CalculatorError, CalculatorError.divisionByZero)
}
}
}
// Production code
class Calculator {
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
func divide(_ a: Int, _ b: Int) throws -> Int {
guard b != 0 else {
throw CalculatorError.divisionByZero
}
return a / b
}
}
enum CalculatorError: Error {
case divisionByZero
}
Example 2: Testing Asynchronous Code
User Prompt: "How do I write unit tests for asynchronous functions using expectations?"
Expected Output:
import XCTest
@testable import MyApp
class NetworkServiceTests: XCTestCase {
var networkService: NetworkService!
var mockURLSession: MockURLSession!
override func setUp() {
super.setUp()
mockURLSession = MockURLSession()
networkService = NetworkService(session: mockURLSession)
}
override func tearDown() {
networkService = nil
mockURLSession = nil
super.tearDown()
}
func testFetchDataSuccess() {
// Given
let expectation = expectation(description: "Fetch data completes")
let expectedData = "Hello, World!".data(using: .utf8)!
mockURLSession.data = expectedData
mockURLSession.response = HTTPURLResponse(url: URL(string: "https://example.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
// When
networkService.fetchData(from: URL(string: "https://example.com")!) { result in
// Then
switch result {
case .success(let data):
XCTAssertEqual(data, expectedData)
case .failure:
XCTFail("Expected success but got failure")
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
func testFetchDataFailure() {
// Given
let expectation = expectation(description: "Fetch data fails")
let expectedError = URLError(.notConnectedToInternet)
mockURLSession.error = expectedError
// When
networkService.fetchData(from: URL(string: "https://example.com")!) { result in
// Then
switch result {
case .success:
XCTFail("Expected failure but got success")
case .failure(let error):
XCTAssertEqual((error as? URLError)?.code, .notConnectedToInternet)
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
// Modern async/await testing (iOS 15+)
@available(iOS 15.0, *)
func testFetchDataAsync() async throws {
// Given
let expectedData = "Hello, World!".data(using: .utf8)!
mockURLSession.data = expectedData
mockURLSession.response = HTTPURLResponse(url: URL(string: "https://example.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
// When
let data = try await networkService.fetchDataAsync(from: URL(string: "https://example.com")!)
// Then
XCTAssertEqual(data, expectedData)
}
}
// Mock classes
class MockURLSession: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let task = MockURLSessionDataTask()
task.completionHandler = {
completionHandler(self.data, self.response, self.error)
}
return task
}
}
class MockURLSessionDataTask: URLSessionDataTask {
var completionHandler: (() -> Void)?
override func resume() {
completionHandler?()
}
}
// Production code
class NetworkService {
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func fetchData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
session.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
completion(.success(data))
}
}.resume()
}
@available(iOS 15.0, *)
func fetchDataAsync(from url: URL) async throws -> Data {
let (data, _) = try await session.data(from: url)
return data
}
}
Example 3: Mocking with Protocols
User Prompt: "Create unit tests with protocol-based mocking for dependency injection."
Expected Output:
import XCTest
@testable import MyApp
class UserManagerTests: XCTestCase {
var userManager: UserManager!
var mockUserService: MockUserService!
var mockNotificationCenter: MockNotificationCenter!
override func setUp() {
super.setUp()
mockUserService = MockUserService()
mockNotificationCenter = MockNotificationCenter()
userManager = UserManager(userService: mockUserService,
notificationCenter: mockNotificationCenter)
}
override func tearDown() {
userManager = nil
mockUserService = nil
mockNotificationCenter = nil
super.tearDown()
}
func testLoginSuccess() {
// Given
let user = User(id: 1, name: "John Doe")
mockUserService.loginResult = .success(user)
// When
userManager.login(username: "john", password: "password") { result in
// Then
switch result {
case .success(let loggedInUser):
XCTAssertEqual(loggedInUser.id, user.id)
XCTAssertEqual(loggedInUser.name, user.name)
XCTAssertTrue(self.mockNotificationCenter.postCalled)
case .failure:
XCTFail("Expected success but got failure")
}
}
}
func testLoginFailure() {
// Given
mockUserService.loginResult = .failure(UserServiceError.invalidCredentials)
// When
userManager.login(username: "john", password: "wrong") { result in
// Then
switch result {
case .success:
XCTFail("Expected failure but got success")
case .failure(let error):
XCTAssertEqual(error as? UserServiceError, .invalidCredentials)
XCTAssertFalse(self.mockNotificationCenter.postCalled)
}
}
}
func testLogout() {
// When
userManager.logout()
// Then
XCTAssertTrue(mockUserService.logoutCalled)
XCTAssertTrue(mockNotificationCenter.postCalled)
}
}
// Mock implementations
class MockUserService: UserServiceProtocol {
var loginResult: Result<User, Error>?
var logoutCalled = false
func login(username: String, password: String, completion: @escaping (Result<User, Error>) -> Void) {
if let result = loginResult {
completion(result)
}
}
func logout() {
logoutCalled = true
}
}
class MockNotificationCenter: NotificationCenterProtocol {
var postCalled = false
func post(name: Notification.Name, object: Any?) {
postCalled = true
}
}
// Protocols
protocol UserServiceProtocol {
func login(username: String, password: String, completion: @escaping (Result<User, Error>) -> Void)
func logout()
}
protocol NotificationCenterProtocol {
func post(name: Notification.Name, object: Any?)
}
// Production code
class UserManager {
private let userService: UserServiceProtocol
private let notificationCenter: NotificationCenterProtocol
init(userService: UserServiceProtocol, notificationCenter: NotificationCenterProtocol) {
self.userService = userService
self.notificationCenter = notificationCenter
}
func login(username: String, password: String, completion: @escaping (Result<User, Error>) -> Void) {
userService.login(username: username, password: password) { [weak self] result in
if case .success = result {
self?.notificationCenter.post(name: .userDidLogin, object: nil)
}
completion(result)
}
}
func logout() {
userService.logout()
notificationCenter.post(name: .userDidLogout, object: nil)
}
}
struct User {
let id: Int
let name: String
}
enum UserServiceError: Error {
case invalidCredentials
case networkError
}
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
static let userDidLogout = Notification.Name("userDidLogout")
}
Example 4: Performance Testing
User Prompt: "Write a performance test to ensure a function executes within acceptable time limits."
Expected Output:
import XCTest
@testable import MyApp
class PerformanceTests: XCTestCase {
var dataProcessor: DataProcessor!
override func setUp() {
super.setUp()
dataProcessor = DataProcessor()
}
override func tearDown() {
dataProcessor = nil
super.tearDown()
}
func testDataProcessingPerformance() {
// Given
let largeDataset = Array(1...10000)
// When & Then
measure {
_ = dataProcessor.process(data: largeDataset)
}
}
func testDataProcessingPerformanceWithMetrics() {
// Given
let largeDataset = Array(1...10000)
// When & Then
measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) {
_ = dataProcessor.process(data: largeDataset)
}
}
func testDataProcessingDoesNotExceedTimeLimit() {
// Given
let largeDataset = Array(1...10000)
let maximumAllowedTime: TimeInterval = 0.1 // 100ms
// When
let startTime = Date()
let result = dataProcessor.process(data: largeDataset)
let elapsedTime = Date().timeIntervalSince(startTime)
// Then
XCTAssertLessThan(elapsedTime, maximumAllowedTime,
"Data processing took \(elapsedTime) seconds, which exceeds the limit of \(maximumAllowedTime) seconds")
XCTAssertFalse(result.isEmpty)
}
}
// Production code
class DataProcessor {
func process(data: [Int]) -> [Int] {
// Simulate some processing work
return data.map { $0 * 2 }.filter { $0 > 100 }
}
}
Example 5: UI Testing Basics
User Prompt: "Create a basic UI test for a SwiftUI view with a button and text field."
Expected Output:
import XCTest
class MyAppUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
override func tearDown() {
app = nil
super.tearDown()
}
func testLoginFlow() {
// Given
let usernameField = app.textFields["Username"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Login"]
let welcomeMessage = app.staticTexts["Welcome!"]
// When
usernameField.tap()
usernameField.typeText("testuser")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
// Then
XCTAssertTrue(welcomeMessage.exists, "Welcome message should appear after successful login")
}
func testLoginValidation() {
// Given
let loginButton = app.buttons["Login"]
let errorMessage = app.staticTexts["Please enter username and password"]
// When
loginButton.tap()
// Then
XCTAssertTrue(errorMessage.exists, "Error message should appear when trying to login without credentials")
}
func testNavigation() {
// Given
let settingsButton = app.buttons["Settings"]
let backButton = app.navigationBars.buttons["Back"]
// When
settingsButton.tap()
// Then
XCTAssertTrue(app.navigationBars["Settings"].exists, "Settings screen should be displayed")
// When
backButton.tap()
// Then
XCTAssertTrue(app.navigationBars["Home"].exists, "Should navigate back to home screen")
}
}
Note: UI tests require setting up accessibility identifiers in your SwiftUI views:
struct LoginView: View {
@State private var username = ""
@State private var password = ""
var body: some View {
VStack {
TextField("Username", text: $username)
.accessibility(identifier: "Username")
SecureField("Password", text: $password)
.accessibility(identifier: "Password")
Button("Login") {
// Login logic
}
.accessibility(identifier: "Login")
}
}
}
More by pstuart
View allComprehensive guide to Fastlane automation for iOS and Android app deployment. Use when helping with mobile app releases, code signing, screenshots, TestFlight, App Store Connect, Google Play Store, beta distribution, CI/CD pipelines, or any Fastlane actions/lanes. Covers gym, match, pilot, deliver, supply, snapshot, screengrab, and 100+ other actions.
Expert guidance for Nuxt 4 projects with app/ directory structure, Vue 3 Composition API, TypeScript, Vitest, ESLint, and Tailwind CSS 4 with Vite plugin. Use when creating components, composables, tests, or any frontend code. Enforces Tailwind-only styling (no inline styles or custom CSS). Supports Nuxt layers.
Guide for building iOS apps using Swift 6, iOS 18+, SwiftUI, SwiftData, and modern concurrency patterns. Use when writing Swift/iOS code, designing app architecture, or modernizing legacy patterns. Prevents outdated patterns like Core Data, ObservableObject, DispatchQueue, and NavigationView.
Expert guide for Apple's modern Speech framework (macOS 26+, iOS 26+) featuring SpeechAnalyzer and SpeechTranscriber for on-device speech-to-text transcription.
