Loading...
Loading...
This skill should be used when the user asks to "write tests", "add unit tests", "implement UI tests", "analyze test coverage", "use XCTest", "use Swift Testing", "test SwiftUI views", or needs iOS/macOS testing guidance. Provides testing strategies, XCTest and Swift Testing framework patterns, and SwiftUI-specific testing techniques.
npx skill4agent add kmshdev/claude-swift-toolkit ios-testing/testtest_macostest_sim| Framework | When to Use |
|---|---|
Swift Testing ( | New test code. Preferred for modern projects. |
XCTest ( | Legacy tests, UI tests, performance tests. |
import Testing
@testable import MyApp
@Suite("User Model Tests")
struct UserTests {
@Test("Creating user with valid email succeeds")
func createUserWithValidEmail() {
let user = User(name: "Alice", email: "alice@example.com")
#expect(user.name == "Alice")
#expect(user.email == "alice@example.com")
}
@Test("Creating user with empty name throws")
func createUserWithEmptyName() {
#expect(throws: ValidationError.emptyName) {
try User(name: "", email: "alice@example.com")
}
}
}@Test("Email validation", arguments: [
("valid@test.com", true),
("no-at-sign", false),
("@no-local.com", false),
("user@.com", false),
])
func emailValidation(email: String, isValid: Bool) {
#expect(Email.isValid(email) == isValid)
}@Test("Fetching items returns non-empty list")
func fetchItems() async throws {
let service = ItemService(repository: MockRepository())
let items = try await service.fetchAll()
#expect(!items.isEmpty)
}extension Tag {
@Tag static var networking: Self
@Tag static var persistence: Self
}
@Test("API call succeeds", .tags(.networking))
func apiCall() async throws { ... }import XCTest
@testable import MyApp
final class UserServiceTests: XCTestCase {
var sut: UserService!
var mockRepository: MockUserRepository!
override func setUp() {
super.setUp()
mockRepository = MockUserRepository()
sut = UserService(repository: mockRepository)
}
override func tearDown() {
sut = nil
mockRepository = nil
super.tearDown()
}
func testFetchUsersReturnsExpectedCount() async throws {
mockRepository.stubbedUsers = [User.sample, User.sample2]
let users = try await sut.fetchUsers()
XCTAssertEqual(users.count, 2)
}
}func testPublisherEmitsValue() {
let expectation = expectation(description: "Value received")
let cancellable = viewModel.$items
.dropFirst()
.sink { items in
XCTAssertFalse(items.isEmpty)
expectation.fulfill()
}
viewModel.loadItems()
wait(for: [expectation], timeout: 5.0)
cancellable.cancel()
}@Test("ViewModel loads items on fetch")
func viewModelLoadsItems() async {
let viewModel = ItemListViewModel(repository: MockRepository(items: Item.samples))
await viewModel.fetch()
#expect(viewModel.items.count == 3)
#expect(viewModel.isLoading == false)
}@Test("Inserting model persists correctly")
func insertModel() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Item.self, configurations: config)
let context = container.mainContext
let item = Item(title: "Test", createdAt: .now)
context.insert(item)
try context.save()
let fetched = try context.fetch(FetchDescriptor<Item>())
#expect(fetched.count == 1)
#expect(fetched.first?.title == "Test")
}@Test("Deleting parent cascades to children")
func cascadeDelete() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Folder.self, Document.self, configurations: config)
let context = container.mainContext
let folder = Folder(name: "Work")
let doc = Document(title: "Notes", folder: folder)
context.insert(folder)
try context.save()
context.delete(folder)
try context.save()
let docs = try context.fetch(FetchDescriptor<Document>())
#expect(docs.isEmpty)
}@SuiteModelContainerFatal error: This model instance was destroyed by calling ModelContext.reset@MainActor
@Suite("MyModel Tests", .serialized) // Forces sequential execution
struct MyModelTests {
private func makeContainer() throws -> ModelContainer {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
return try ModelContainer(for: MyModel.self, configurations: config)
}
@Test("insert persists")
func insert() throws {
let container = try makeContainer()
let context = container.mainContext
// container stays alive for the duration of this test
// ...
}
}.serialized@Test("insert persists")
func insert() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: MyModel.self, configurations: config)
let context = container.mainContext
// container is local to this test — no cross-test interference
// ...
}ModelContainerModelContext@Model// WRONG — container dies when helper returns
func makeContext() throws -> ModelContext {
let container = try ModelContainer(...) // local → deallocated on return
return container.mainContext // orphaned context
}
// RIGHT — return the container (or have the owner retain it)
func makeContainer() throws -> ModelContainer {
return try ModelContainer(for: MyModel.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true))
}protocol ItemRepository: Sendable {
func fetchAll() async throws -> [Item]
func save(_ item: Item) async throws
}
// Production
final class RemoteItemRepository: ItemRepository {
func fetchAll() async throws -> [Item] { /* network call */ }
func save(_ item: Item) async throws { /* network call */ }
}
// Test mock
final class MockItemRepository: ItemRepository {
var stubbedItems: [Item] = []
var savedItems: [Item] = []
func fetchAll() async throws -> [Item] { stubbedItems }
func save(_ item: Item) async throws { savedItems.append(item) }
}@Test("Item title updates correctly")
func updateTitle() {
// Arrange
let item = Item(title: "Old")
// Act
item.title = "New"
// Assert
#expect(item.title == "New")
}@Test("Descriptive behavior")test<Unit>_<Condition>_<ExpectedResult>()testUserService_EmptyEmail_ThrowsValidationError()isStoredInMemoryOnly: true.serialized@TestModelContainerModelContextasync throwstemplates/ViewModelTestTemplate.swift@Suite@TestMockRepositoryTemplate.swift@unchecked Sendable