本文へ移動
cccskills
無料GitHub で公開

iOS Testing

Unit testing, UI testing, TDD patterns, mocking, and test doubles for VIP+W architecture

インストール方法を見る

含まれるファイル(1)

  • SKILL.md17.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

iOS Testing

Comprehensive testing strategies for iOS applications using VIP+W architecture, including unit tests, UI tests, and TDD patterns.

Testing VIP+W Architecture

Test Structure for VIP+W

Tests/
├── Scenes/
│   └── Login/
│       ├── LoginInteractorTests.swift
│       ├── LoginPresenterTests.swift
│       ├── LoginWorkerTests.swift
│       ├── LoginViewControllerTests.swift
│       └── Mocks/
│           ├── MockLoginPresenter.swift
│           ├── MockLoginWorker.swift
│           └── MockLoginDisplayLogic.swift
├── Workers/
│   └── APIClientTests.swift
├── Helpers/
│   └── XCTestCase+Async.swift
└── Mocks/
    └── MockURLSession.swift

Testing the Interactor

import XCTest
@testable import MyApp

final class LoginInteractorTests: XCTestCase {

    // MARK: - Subject Under Test

    var sut: LoginInteractor!
    var mockPresenter: MockLoginPresenter!
    var mockWorker: MockLoginWorker!

    // MARK: - Setup

    override func setUp() {
        super.setUp()
        mockPresenter = MockLoginPresenter()
        mockWorker = MockLoginWorker()
        sut = LoginInteractor(worker: mockWorker)
        sut.presenter = mockPresenter
    }

    override func tearDown() {
        sut = nil
        mockPresenter = nil
        mockWorker = nil
        super.tearDown()
    }

    // MARK: - Tests

    func test_authenticate_withValidCredentials_shouldPresentSuccess() async {
        // Given
        let request = Login.Authenticate.Request(
            email: "test@example.com",
            password: "password123"
        )
        mockWorker.loginResult = .success(User.mock)

        // When
        sut.authenticate(request: request)

        // Then
        await waitForAsync()
        XCTAssertTrue(mockPresenter.presentLoginResultCalled)
        XCTAssertNotNil(mockPresenter.lastLoginResponse)

        if case .success(let user) = mockPresenter.lastLoginResponse?.result {
            XCTAssertEqual(user.email, "test@example.com")
        } else {
            XCTFail("Expected success result")
        }
    }

    func test_authenticate_withInvalidCredentials_shouldPresentError() async {
        // Given
        let request = Login.Authenticate.Request(
            email: "test@example.com",
            password: "wrong"
        )
        mockWorker.loginResult = .failure(.invalidCredentials)

        // When
        sut.authenticate(request: request)

        // Then
        await waitForAsync()
        XCTAssertTrue(mockPresenter.presentLoginResultCalled)

        if case .failure(let error) = mockPresenter.lastLoginResponse?.result {
            XCTAssertEqual(error, .invalidCredentials)
        } else {
            XCTFail("Expected failure result")
        }
    }

    func test_authenticate_shouldShowLoadingState() async {
        // Given
        let request = Login.Authenticate.Request(
            email: "test@example.com",
            password: "password123"
        )
        mockWorker.loginResult = .success(User.mock)

        // When
        sut.authenticate(request: request)

        // Then
        XCTAssertTrue(mockPresenter.presentLoadingCalled)
        XCTAssertTrue(mockPresenter.lastLoadingState == true)

        await waitForAsync()
        XCTAssertTrue(mockPresenter.lastLoadingState == false)
    }
}

Testing the Presenter

import XCTest
@testable import MyApp

final class LoginPresenterTests: XCTestCase {

    // MARK: - Subject Under Test

    var sut: LoginPresenter!
    var mockViewController: MockLoginViewController!

    // MARK: - Setup

    override func setUp() {
        super.setUp()
        mockViewController = MockLoginViewController()
        sut = LoginPresenter()
        sut.viewController = mockViewController
    }

    override func tearDown() {
        sut = nil
        mockViewController = nil
        super.tearDown()
    }

    // MARK: - Tests

    func test_presentLoginResult_withSuccess_shouldDisplayWelcomeMessage() {
        // Given
        let user = User(id: "1", name: "John", email: "john@test.com")
        let response = Login.Authenticate.Response(result: .success(user))

        // When
        sut.presentLoginResult(response: response)

        // Then
        XCTAssertTrue(mockViewController.displayLoginResultCalled)
        XCTAssertTrue(mockViewController.lastLoginViewModel?.isSuccess == true)
        XCTAssertEqual(
            mockViewController.lastLoginViewModel?.welcomeMessage,
            "Welcome, John!"
        )
        XCTAssertNil(mockViewController.lastLoginViewModel?.errorMessage)
    }

    func test_presentLoginResult_withFailure_shouldDisplayErrorMessage() {
        // Given
        let response = Login.Authenticate.Response(
            result: .failure(.invalidCredentials)
        )

        // When
        sut.presentLoginResult(response: response)

        // Then
        XCTAssertTrue(mockViewController.displayLoginResultCalled)
        XCTAssertFalse(mockViewController.lastLoginViewModel?.isSuccess ?? true)
        XCTAssertNotNil(mockViewController.lastLoginViewModel?.errorMessage)
        XCTAssertNil(mockViewController.lastLoginViewModel?.welcomeMessage)
    }

    func test_presentValidationError_withInvalidEmail_shouldShowEmailError() {
        // Given
        let response = Login.Validate.Response(
            emailError: "Invalid email format",
            passwordError: nil
        )

        // When
        sut.presentValidationError(response: response)

        // Then
        XCTAssertTrue(mockViewController.displayValidationErrorCalled)
        XCTAssertEqual(
            mockViewController.lastValidationViewModel?.emailErrorText,
            "Invalid email format"
        )
        XCTAssertNil(mockViewController.lastValidationViewModel?.passwordErrorText)
        XCTAssertFalse(mockViewController.lastValidationViewModel?.isValid ?? true)
    }
}

Testing the Worker

import XCTest
@testable import MyApp

final class LoginWorkerTests: XCTestCase {

    // MARK: - Subject Under Test

    var sut: LoginWorker!
    var mockAPIClient: MockAPIClient!

    // MARK: - Setup

    override func setUp() {
        super.setUp()
        mockAPIClient = MockAPIClient()
        sut = LoginWorker(apiClient: mockAPIClient)
    }

    override func tearDown() {
        sut = nil
        mockAPIClient = nil
        super.tearDown()
    }

    // MARK: - Tests

    func test_login_withValidCredentials_shouldReturnUser() async throws {
        // Given
        let expectedUser = User(id: "1", name: "John", email: "john@test.com")
        mockAPIClient.mockResponse = LoginResponse(user: expectedUser)

        // When
        let result = try await sut.login(email: "john@test.com", password: "password")

        // Then
        XCTAssertEqual(result.id, expectedUser.id)
        XCTAssertEqual(result.email, expectedUser.email)
    }

    func test_login_withNetworkError_shouldThrowError() async {
        // Given
        mockAPIClient.mockError = NetworkError.noConnection

        // When/Then
        do {
            _ = try await sut.login(email: "test@test.com", password: "password")
            XCTFail("Expected error to be thrown")
        } catch {
            XCTAssertTrue(error is NetworkError)
        }
    }

    func test_validateCredentials_withValidInput_shouldReturnValid() {
        // When
        let result = sut.validateCredentials(
            email: "valid@email.com",
            password: "validPassword123"
        )

        // Then
        XCTAssertTrue(result.isValid)
        XCTAssertNil(result.emailError)
        XCTAssertNil(result.passwordError)
    }

    func test_validateCredentials_withInvalidEmail_shouldReturnError() {
        // When
        let result = sut.validateCredentials(
            email: "invalid-email",
            password: "validPassword123"
        )

        // Then
        XCTAssertFalse(result.isValid)
        XCTAssertNotNil(result.emailError)
    }
}

Mock Objects

Mock Presenter

@testable import MyApp

final class MockLoginPresenter: LoginPresentationLogic {

    // MARK: - Call Tracking

    var presentLoginResultCalled = false
    var presentValidationErrorCalled = false
    var presentLoadingCalled = false

    var lastLoginResponse: Login.Authenticate.Response?
    var lastValidationResponse: Login.Validate.Response?
    var lastLoadingState: Bool?

    // MARK: - PresentationLogic

    func presentLoginResult(response: Login.Authenticate.Response) {
        presentLoginResultCalled = true
        lastLoginResponse = response
    }

    func presentValidationError(response: Login.Validate.Response) {
        presentValidationErrorCalled = true
        lastValidationResponse = response
    }

    func presentLoading(isLoading: Bool) {
        presentLoadingCalled = true
        lastLoadingState = isLoading
    }
}

Mock Worker

@testable import MyApp

final class MockLoginWorker: LoginWorkerLogic {

    // MARK: - Stubbed Results

    var loginResult: Result<User, AuthError> = .failure(.unknown)
    var validationResult = ValidationResult(emailError: nil, passwordError: nil)

    // MARK: - Call Tracking

    var loginCalled = false
    var validateCalled = false

    var lastEmail: String?
    var lastPassword: String?

    // MARK: - WorkerLogic

    func login(email: String, password: String) async throws -> User {
        loginCalled = true
        lastEmail = email
        lastPassword = password

        switch loginResult {
        case .success(let user):
            return user
        case .failure(let error):
            throw error
        }
    }

    func validateCredentials(email: String, password: String) -> ValidationResult {
        validateCalled = true
        return validationResult
    }
}

Mock ViewController

@testable import MyApp

final class MockLoginViewController: LoginDisplayLogic {

    // MARK: - Call Tracking

    var displayLoginResultCalled = false
    var displayValidationErrorCalled = false
    var displayLoadingCalled = false

    var lastLoginViewModel: Login.Authenticate.ViewModel?
    var lastValidationViewModel: Login.Validate.ViewModel?
    var lastLoadingState: Bool?

    // MARK: - DisplayLogic

    func displayLoginResult(viewModel: Login.Authenticate.ViewModel) {
        displayLoginResultCalled = true
        lastLoginViewModel = viewModel
    }

    func displayValidationError(viewModel: Login.Validate.ViewModel) {
        displayValidationErrorCalled = true
        lastValidationViewModel = viewModel
    }

    func displayLoading(isLoading: Bool) {
        displayLoadingCalled = true
        lastLoadingState = isLoading
    }
}

Async Testing Helpers

import XCTest

extension XCTestCase {

    /// Wait for async operations to complete
    func waitForAsync(timeout: TimeInterval = 1.0) async {
        let expectation = expectation(description: "Async wait")

        Task {
            try? await Task.sleep(nanoseconds: UInt64(timeout * 100_000_000))
            expectation.fulfill()
        }

        await fulfillment(of: [expectation], timeout: timeout + 0.5)
    }

    /// Wait for a condition to become true
    func waitFor(
        _ condition: @escaping () -> Bool,
        timeout: TimeInterval = 2.0,
        message: String = "Condition not met"
    ) {
        let expectation = expectation(description: message)

        let timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
            if condition() {
                timer.invalidate()
                expectation.fulfill()
            }
        }

        wait(for: [expectation], timeout: timeout)
        timer.invalidate()
    }
}

UI Testing

Page Object Pattern

import XCTest

// MARK: - Base Page

protocol Page {
    var app: XCUIApplication { get }
}

extension Page {
    func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
        element.waitForExistence(timeout: timeout)
    }
}

// MARK: - Login Page

final class LoginPage: Page {
    let app: XCUIApplication

    init(app: XCUIApplication) {
        self.app = app
    }

    // MARK: - Elements

    var emailTextField: XCUIElement {
        app.textFields["emailTextField"]
    }

    var passwordTextField: XCUIElement {
        app.secureTextFields["passwordTextField"]
    }

    var loginButton: XCUIElement {
        app.buttons["loginButton"]
    }

    var errorLabel: XCUIElement {
        app.staticTexts["errorLabel"]
    }

    var loadingIndicator: XCUIElement {
        app.activityIndicators["loadingIndicator"]
    }

    // MARK: - Actions

    @discardableResult
    func enterEmail(_ email: String) -> Self {
        emailTextField.tap()
        emailTextField.typeText(email)
        return self
    }

    @discardableResult
    func enterPassword(_ password: String) -> Self {
        passwordTextField.tap()
        passwordTextField.typeText(password)
        return self
    }

    @discardableResult
    func tapLogin() -> Self {
        loginButton.tap()
        return self
    }

    func login(email: String, password: String) -> HomePage {
        enterEmail(email)
        enterPassword(password)
        tapLogin()
        return HomePage(app: app)
    }

    // MARK: - Assertions

    func verifyErrorDisplayed(_ message: String) -> Self {
        XCTAssertTrue(errorLabel.waitForExistence(timeout: 5))
        XCTAssertEqual(errorLabel.label, message)
        return self
    }

    func verifyLoginButtonDisabled() -> Self {
        XCTAssertFalse(loginButton.isEnabled)
        return self
    }
}

UI Test Example

import XCTest

final class LoginUITests: XCTestCase {

    var app: XCUIApplication!
    var loginPage: LoginPage!

    override func setUp() {
        super.setUp()
        continueAfterFailure = false

        app = XCUIApplication()
        app.launchArguments = ["--uitesting"]
        app.launch()

        loginPage = LoginPage(app: app)
    }

    override func tearDown() {
        app = nil
        loginPage = nil
        super.tearDown()
    }

    func test_login_withValidCredentials_shouldNavigateToHome() {
        let homePage = loginPage.login(
            email: "test@example.com",
            password: "password123"
        )

        XCTAssertTrue(homePage.welcomeLabel.waitForExistence(timeout: 5))
    }

    func test_login_withInvalidCredentials_shouldShowError() {
        loginPage
            .enterEmail("test@example.com")
            .enterPassword("wrongpassword")
            .tapLogin()
            .verifyErrorDisplayed("Invalid credentials")
    }

    func test_login_withEmptyFields_shouldDisableButton() {
        loginPage.verifyLoginButtonDisabled()
    }
}

Test-Driven Development (TDD)

Red-Green-Refactor Cycle

// 1. RED - Write a failing test first
func test_validateEmail_withInvalidFormat_shouldReturnError() {
    let validator = EmailValidator()
    let result = validator.validate("invalid-email")
    XCTAssertFalse(result.isValid)
    XCTAssertEqual(result.error, "Invalid email format")
}

// 2. GREEN - Write minimal code to pass
struct EmailValidator {
    func validate(_ email: String) -> ValidationResult {
        let isValid = email.contains("@") && email.contains(".")
        return ValidationResult(
            isValid: isValid,
            error: isValid ? nil : "Invalid email format"
        )
    }
}

// 3. REFACTOR - Improve the code
struct EmailValidator {
    private let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"

    func validate(_ email: String) -> ValidationResult {
        let predicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
        let isValid = predicate.evaluate(with: email)
        return ValidationResult(
            isValid: isValid,
            error: isValid ? nil : "Invalid email format"
        )
    }
}

Testing Best Practices

Naming Convention

// Pattern: test_[methodName]_[scenario]_[expectedBehavior]

func test_authenticate_withValidCredentials_shouldReturnUser() { }
func test_authenticate_withExpiredToken_shouldRefreshAndRetry() { }
func test_fetchUsers_withNetworkError_shouldReturnCachedData() { }

Arrange-Act-Assert (AAA)

func test_calculateTotal_withDiscountCode_shouldApplyDiscount() {
    // Arrange (Given)
    let cart = ShoppingCart()
    cart.addItem(Item(price: 100))
    cart.applyDiscountCode("SAVE20")

    // Act (When)
    let total = cart.calculateTotal()

    // Assert (Then)
    XCTAssertEqual(total, 80)
}

Test Data Builders

// MARK: - User Builder

extension User {
    static var mock: User {
        User(id: "mock-id", name: "Mock User", email: "mock@test.com")
    }

    static func mock(
        id: String = "mock-id",
        name: String = "Mock User",
        email: String = "mock@test.com"
    ) -> User {
        User(id: id, name: name, email: email)
    }
}

// Usage
let user = User.mock(name: "Custom Name")

Test Coverage Checklist

Interactor Tests

  • Happy path for each use case
  • Error handling
  • Loading state management
  • Data store updates
  • Edge cases (empty data, nil values)

Presenter Tests

  • Formatting logic
  • ViewModel construction
  • Error message formatting
  • Date/number formatting

Worker Tests

  • API success responses
  • API error responses
  • Network failures
  • Validation logic
  • Caching behavior

ViewController Tests

  • Initial state
  • User interactions trigger correct interactor methods
  • ViewModel updates UI correctly

UI Tests

  • Critical user flows
  • Error states
  • Loading states
  • Accessibility

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

When the user wants to A/B test App Store product page elements to improve conversion rate. Also use when the user mentions "A/B test", "product page optimization", "test my screenshots", "test my icon", "conversion rate optimization", "CPP", or "custom product pages". For screenshot design, see screenshot-optimization. For metadata optimization, see metadata-optimization.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Discover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Implement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Measure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

alarmkit

無料

Implement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, and Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, AlarmButton stop and snooze actions, authorization, state observation, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

When the user wants to optimize their Google Play Store listing — title, short description, full description, keywords, ratings, or Play Store-specific features. Use when the user mentions "Google Play", "Android", "Play Store", "Play Console", "short description", "full description indexed", "Google Play ASO", or wants Google Play-specific keyword, creative, or ratings strategy. For iOS App Store optimization, see aso-audit and metadata-optimization.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

JordanCoin のスキルをすべて見る

このスキルの問題を報告する