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.
npx -y skills add JordanCoin/ios-skills-collection --skill arimunandar--ios-testingAssembled 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