swift unit testing skill
npx skills add https://github.com/fal3/claude-skills-collection --skill Swift Unit Testing Skill
Skill 文档
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")
}
}
}