agentsclimarketplace

Arimunandar ios testing

Skill JordanCoin/ios-skills-collection/skills/arimunandar--ios-testing

200+ iOS/Swift/Xcode agent skills from 28 authors with phase-aware routing. Claude Code plugin + Codex CLI + Mac App. Full lifecycle: ideation → design → develop → test → deploy → iterate.

Install
npx -y skills add JordanCoin/ios-skills-collection --skill arimunandar--ios-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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

SKILL.md

17.6 KB, as published. Nobody here has run it

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: "[email protected]",
            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, "[email protected]")
        } else {
            XCTFail("Expected success result")
        }
    }

    func test_authenticate_withInvalidCredentials_shouldPresentError() async {
        // Given
        let request = Login.Authenticate.Request(
            email: "[email protected]",
            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: "[email protected]",
            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: "[email protected]")
        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: "[email protected]")
        mockAPIClient.mockResponse = LoginResponse(user: expectedUser)

        // When
        let result = try await sut.login(email: "[email protected]", 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: "[email protected]", password: "password")
            XCTFail("Expected error to be thrown")
        } catch {
            XCTAssertTrue(error is NetworkError)
        }
    }

    func test_validateCredentials_withValidInput_shouldReturnValid() {
        // When
        let result = sut.validateCredentials(
            email: "[email protected]",
            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: "[email protected]",
            password: "password123"
        )

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

    func test_login_withInvalidCredentials_shouldShowError() {
        loginPage
            .enterEmail("[email protected]")
            .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: "[email protected]")
    }

    static func mock(
        id: String = "mock-id",
        name: String = "Mock User",
        email: String = "[email protected]"
    ) -> 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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.