From swiftui-dev
Provides XCTest unit testing strategies for SwiftUI apps including ViewModels, @Observable classes, async code, plus UI tests and preview-based testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/swiftui-dev:swiftui-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Testing strategies for SwiftUI applications including unit tests, UI tests, and preview-based testing.
Testing strategies for SwiftUI applications including unit tests, UI tests, and preview-based testing.
import XCTest
@testable import MyApp
final class ItemViewModelTests: XCTestCase {
var sut: ItemViewModel!
var mockRepository: MockItemRepository!
override func setUp() {
super.setUp()
mockRepository = MockItemRepository()
sut = ItemViewModel(repository: mockRepository)
}
override func tearDown() {
sut = nil
mockRepository = nil
super.tearDown()
}
func test_loadItems_success_populatesItems() async {
// Given
let expectedItems = [Item(id: "1", name: "Test")]
mockRepository.items = expectedItems
// When
await sut.loadItems()
// Then
XCTAssertEqual(sut.items, expectedItems)
XCTAssertFalse(sut.isLoading)
XCTAssertNil(sut.errorMessage)
}
func test_loadItems_failure_setsError() async {
// Given
mockRepository.shouldFail = true
// When
await sut.loadItems()
// Then
XCTAssertTrue(sut.items.isEmpty)
XCTAssertNotNil(sut.errorMessage)
}
}
import XCTest
@testable import MyApp
final class ShoppingCartTests: XCTestCase {
func test_addProduct_increasesQuantity() {
// Given
let cart = ShoppingCart()
let product = Product(id: "1", name: "Widget", price: 9.99)
// When
cart.add(product)
cart.add(product)
// Then
XCTAssertEqual(cart.items.count, 1)
XCTAssertEqual(cart.items.first?.quantity, 2)
}
func test_total_calculatesCorrectly() {
// Given
let cart = ShoppingCart()
cart.items = [
CartItem(productId: "1", price: 10.00, quantity: 2),
CartItem(productId: "2", price: 5.00, quantity: 1)
]
// Then
XCTAssertEqual(cart.total, 25.00)
}
}
func test_fetchData_withCancellation() async {
// Given
let viewModel = DataViewModel()
let task = Task {
await viewModel.fetchData()
}
// When
task.cancel()
await task.value
// Then
XCTAssertTrue(Task.isCancelled)
}
func test_fetchData_withTimeout() async throws {
// Given
let viewModel = SlowViewModel()
// When/Then - should complete within 5 seconds
try await withTimeout(seconds: 5) {
await viewModel.fetchData()
}
}
// Helper
func withTimeout<T>(seconds: Double, operation: @escaping () async throws -> T) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await operation()
}
group.addTask {
try await Task.sleep(for: .seconds(seconds))
throw TimeoutError()
}
let result = try await group.next()!
group.cancelAll()
return result
}
}
import XCTest
final class ItemListUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["--uitesting"]
app.launch()
}
func test_addItem_appearsInList() {
// Given
let addButton = app.buttons["addItemButton"]
let nameField = app.textFields["itemNameField"]
let saveButton = app.buttons["saveButton"]
// When
addButton.tap()
nameField.tap()
nameField.typeText("New Item")
saveButton.tap()
// Then
let newItem = app.staticTexts["New Item"]
XCTAssertTrue(newItem.waitForExistence(timeout: 2))
}
}
// Page object for Item List screen
struct ItemListPage {
let app: XCUIApplication
var addButton: XCUIElement {
app.buttons["addItemButton"]
}
var itemList: XCUIElement {
app.collectionViews["itemList"]
}
func item(named name: String) -> XCUIElement {
app.staticTexts[name]
}
func tapAdd() -> AddItemPage {
addButton.tap()
return AddItemPage(app: app)
}
func waitForItems() {
_ = itemList.waitForExistence(timeout: 5)
}
}
// Page object for Add Item screen
struct AddItemPage {
let app: XCUIApplication
var nameField: XCUIElement {
app.textFields["itemNameField"]
}
var saveButton: XCUIElement {
app.buttons["saveButton"]
}
func enterName(_ name: String) -> Self {
nameField.tap()
nameField.typeText(name)
return self
}
func save() -> ItemListPage {
saveButton.tap()
return ItemListPage(app: app)
}
}
// Usage in test
func test_addItem_pageObject() {
let listPage = ItemListPage(app: app)
listPage.waitForItems()
let result = listPage
.tapAdd()
.enterName("New Item")
.save()
XCTAssertTrue(result.item(named: "New Item").exists)
}
// In SwiftUI View
struct ItemRow: View {
let item: Item
var body: some View {
HStack {
Text(item.name)
Spacer()
Button("Delete") {
// delete action
}
.accessibilityIdentifier("deleteButton-\(item.id)")
}
.accessibilityIdentifier("itemRow-\(item.id)")
}
}
// In UI Test
func test_deleteItem() {
let deleteButton = app.buttons["deleteButton-item123"]
deleteButton.tap()
let itemRow = app.otherElements["itemRow-item123"]
XCTAssertFalse(itemRow.exists)
}
import XCTest
import ViewInspector
@testable import MyApp
extension ItemRow: Inspectable {}
final class ItemRowTests: XCTestCase {
func test_displaysItemName() throws {
// Given
let item = Item(id: "1", name: "Test Item")
let view = ItemRow(item: item)
// When
let text = try view.inspect().find(text: "Test Item")
// Then
XCTAssertNotNil(text)
}
func test_deleteButton_callsOnDelete() throws {
// Given
var deleteCalled = false
let item = Item(id: "1", name: "Test")
let view = ItemRow(item: item, onDelete: { deleteCalled = true })
// When
try view.inspect().find(button: "Delete").tap()
// Then
XCTAssertTrue(deleteCalled)
}
}
import XCTest
import SnapshotTesting
@testable import MyApp
final class ItemRowSnapshotTests: XCTestCase {
func test_itemRow_lightMode() {
let view = ItemRow(item: .preview)
.frame(width: 300)
.environment(\.colorScheme, .light)
assertSnapshot(of: view, as: .image)
}
func test_itemRow_darkMode() {
let view = ItemRow(item: .preview)
.frame(width: 300)
.environment(\.colorScheme, .dark)
assertSnapshot(of: view, as: .image)
}
func test_itemRow_dynamicType() {
for category in [ContentSizeCategory.extraSmall, .large, .accessibilityExtraExtraLarge] {
let view = ItemRow(item: .preview)
.frame(width: 300)
.environment(\.sizeCategory, category)
assertSnapshot(of: view, as: .image, named: "\(category)")
}
}
}
// Sample data for previews
extension Item {
static var preview: Item {
Item(id: "preview", name: "Preview Item", description: "A sample item")
}
static var previews: [Item] {
[
Item(id: "1", name: "First Item"),
Item(id: "2", name: "Second Item"),
Item(id: "3", name: "Third Item with a very long name that might wrap")
]
}
}
// Comprehensive preview
#Preview("Item Row - Standard") {
ItemRow(item: .preview)
}
#Preview("Item Row - Long Name") {
ItemRow(item: Item(id: "1", name: "This is a very long item name"))
}
#Preview("Item Row - Dark Mode") {
ItemRow(item: .preview)
.preferredColorScheme(.dark)
}
#Preview("Item List - Multiple") {
List(Item.previews) { item in
ItemRow(item: item)
}
}
Tests/
├── UnitTests/
│ ├── ViewModels/
│ │ └── ItemViewModelTests.swift
│ ├── Models/
│ │ └── ItemTests.swift
│ └── Services/
│ └── ItemRepositoryTests.swift
├── UITests/
│ ├── Pages/
│ │ ├── ItemListPage.swift
│ │ └── AddItemPage.swift
│ └── Flows/
│ └── ItemManagementTests.swift
└── SnapshotTests/
└── ItemRowSnapshotTests.swift
npx claudepluginhub arustydev/agents --plugin swiftui-devProvides XCTest patterns for unit tests, UI tests, mocks, async/await testing, and property testing in Swift/SwiftUI iOS apps.
Provides guidelines, templates, and examples for writing XCTest unit tests in Swift, covering TDD practices, mocking techniques, and best practices.
Guides Swift Testing code writing, review, and migration with patterns for structs over classes, async confirmations, parameterized tests, exit tests, attachments, and common pitfalls.