Testing patterns, unit tests, UI tests, mocks, and test infrastructure.
Tests/
├── MessageEvaluationManagerTests.swift # Core filtering logic
├── AutomaticFilterManagerTests.swift # Cloud filter management
├── PersistanceManagerTests.swift # CoreData operations
├── FilterListViewModelTests.swift # ViewModel integration
├── FilterTransferManagerTests.swift # Merge-only import/export picker
├── FlowManagerTests.swift # Launch-order queue
├── DefaultsManagerTests.swift # Accent RGB storage / Color round-trip
├── AmazonS3ServiceTests.swift # HTTP service
└── Mocks/
├── mock_AppManager.swift
├── mock_PersistanceManager.swift
├── mock_MessageEvaluationManager.swift
├── mock_AutomaticFilterManager.swift
├── mock_DefaultsManager.swift
├── mock_NetworkSyncManager.swift
├── mock_AmazonS3Service.swift
├── mock_HTTPService.swift
├── mock_ReportMessageService.swift
├── mock_FilterTransferManager.swift
└── mock_FlowManager.swift
UI Tests/
├── ApplicationTestCase.swift # Base XCTestCase
├── TestApplication.swift # XCUIApplication wrapper
├── SnapshotsTestCase.swift # Full UI flow + screenshots (iPhone & iPad)
├── UITestsHelpers.swift # Localization + XCUIElement helpers
└── SnapshotHelper.swift # Fastlane snapshot integration
fastlane/
└── Fastfile # Screenshot lanes (iphone/ipad/both)
Tests: MessageEvaluationManager — the core filtering engine.
Setup: In-memory CoreData (MessageEvaluationManager(inMemory: true) — owned store loads synchronously in init). Each test flushes all entities (Filter, AutomaticFiltersCache, AutomaticFiltersLanguage, AutomaticFiltersRule) and re-populates with specific test data.
Test approach: Uses a MessageTestCase struct with sender, body, expectedAction fields. Iterates over arrays of test cases and asserts each result.
Coverage areas:
- Allow/deny filter matching with various target/matching/case combinations
- Language detection and blocking (Hebrew, English, Arabic)
- Automatic filter lists (allow senders, deny body, etc.)
- Smart rules (links, numbers-only senders, short senders, emails, emojis, all-unknown, country allowlist)
- Priority ordering (allow takes precedence over deny)
- Edge cases (empty body/sender, combined targets)
Multi-language test data: Tests include Hebrew, English, and Arabic text to verify NLLanguageRecognizer integration.
Tests: AutomaticFilterManager — cloud filter management.
Setup: Mock dependencies (mock_PersistanceManager, mock_AmazonS3Service). Configures closures on mocks to return specific data for each test.
Coverage areas:
isAutomaticFilteringOn— on/off states based on cache + language recordsactiveAutomaticFiltersTitle— localized comma-separated language namesautomaticFiltersCacheAge— timestamp propagationlanguages(for:)— both.blockLanguageand.automaticBlockingmodes- Language state get/set with context commits
- Rule state get/set with
ensureAutomaticFiltersRuleRecord selectedChoiceget/set for configurable rulesforceUpdateAutomaticFilters— async fetch + save cache flow
Verifies call counts on mocks to ensure correct manager interactions.
Tests: PersistanceManager — CoreData CRUD operations.
Setup: In-memory CoreData (PersistanceManager(inMemory: true)). Observes NSManagedObjectContextDidSave notifications to verify writes.
Coverage areas:
- Fetch all filters / fetch by type
- Fetch automatic filter language/rule/cache records
- Fetch single language/rule records
isDuplicateFilter— false before add, true after- Add filter (deny and allow) — verifies notification fires
- Delete filters by IndexSet and by Set
- Update filter properties (folder, matching, case, target, text)
- Save/update cache — hash, data, age verification
isCacheStale— same data = false, different data = true
Notification testing pattern:
func expectingSaveContext() {
let expectASave = expectation(description: "expectASave")
waitForSavedNotification { _ in expectASave.fulfill() }
}
// Then: waitForExpectations(timeout: 1)Tests: FilterListView.ViewModel — view model integration with mocked managers.
Setup: Injects mock_AppManager with mock sub-managers.
Coverage:
refresh()— verifies it fetches records, checks allUnknown state, checks language availabilitydeleteFilters(withOffsets:)— verifies PersistanceManager called + refreshdeleteFilters(_:)— verifies PersistanceManager called + refresh
Tests: DefaultsManager accent storage via UserDefaults.standard (DefaultsManager(), no injected store).
Coverage:
- Empty
accentColorRGBmeans no custom color (Color(accentRGB:)is nil) - Writing RGB round-trips through
Color.accentRGB/Color(accentRGB:)
Tests: AmazonS3Service — HTTP request construction.
Setup: Mock HTTP service (mock_HTTPService).
Verifies: Correct URL, path, HTTP method, response type, and request task type for the S3 fetch.
All mocks follow a consistent pattern with three components:
Every method and property getter/setter has a counter:
var fetchFilterRecordsCounter = 0
var addFilterCounter = 0
var isAutomaticFilteringOnGetCounter = 0 // separate get/set for properties
var isAutomaticFilteringOnSetCounter = 0Each method has an optional closure for configuring return values:
var fetchFilterRecordsClosure: (() -> [Filter])?
var isDuplicateFilterClosure: ((String, FilterTarget, FilterMatching, FilterCase) -> Bool)?All mocks have resetCounters() to clear state between tests:
func resetCounters() {
fetchFilterRecordsCounter = 0
addFilterCounter = 0
// ... all counters
}// Arrange — configure mock behavior
persistanceManager.isDuplicateFilterClosure = { text, target, matching, filterCase in
return true
}
// Act
let result = testSubject.isDuplicateFilter
// Assert — verify behavior AND call count
XCTAssertTrue(result)
XCTAssertEqual(persistanceManager.isDuplicateFilterCounter, 1)- mock_PersistanceManager — Largest mock (~41 counters). Wraps a real in-memory
PersistanceManagerforcontextaccess, allowing tests to create real CoreData objects. - mock_MessageEvaluationManager — Also wraps a real in-memory instance for
context. - mock_HTTPService — Uses type-switching in
execute()to return appropriate mock responses based on the generic type parameter. - mock_AppManager — Pre-populated with all mock sub-managers. Tests replace individual managers as needed.
- ApplicationTestCase — Base
XCTestCasewithcontinueAfterFailure = falseand asleep(seconds:)helper using inverted expectations. - TestApplication —
XCUIApplicationwrapper. Adds-Testinglaunch argument (triggers data reset in AppManager). Sets up Fastlane snapshots.
The app detects testing mode via:
// SharedUITestsHelpers.swift (DEBUG only)
extension UIApplication {
var isInTestingMode: Bool {
return ProcessInfo.processInfo.arguments.contains("-Testing")
}
}When detected, AppManager.init() resets DefaultsManager and PersistanceManager for a clean slate.
| Method | Purpose |
|---|---|
dismissCallToActionViewIfPresented() |
Dismiss onboarding overlay |
addFilter(type:text:denyFolderType:filterTarget:filterMatching:filterCase:) |
Complete filter creation flow via UI |
button(_ id: TestIdentifier) |
Find button by accessibility ID |
textField(_ id: TestIdentifier) |
Find text field by accessibility ID |
tap(_ id: TestIdentifier) |
Tap element by accessibility ID |
assertLabel(of:contains:) |
Verify button label content |
buttonContaining(_ label: String) |
Find button by label predicate |
switchContaining(_ label: String) |
Find switch by label predicate |
Single test method testCreateSnapshots() that runs a full UI flow:
- Enable English + Hebrew automatic language filters
- Add a deny filter (language-specific test text)
- Enable all smart rules + load debug data
- Navigate through deny/allow/language filter lists
- Test filters with a sample message ("Your Apple ID Code is: 444291...")
- Capture Fastlane screenshots at each step
iPad support: Uses an isPad computed property to skip back-button taps and swipe gestures that aren't needed in split view. Sets XCUIDevice.shared.orientation = .landscapeRight before app launch for landscape screenshots.
The ~ postfix operator is redefined in UITestsHelpers.swift to load strings from the test bundle (not the app bundle):
postfix func ~ (string: String) -> String {
guard let path = Bundle(for: SnapshotsTestCase.self)
.path(forResource: Locale.current.languageCode, ofType: "lproj"),
let bundle = Bundle(path: path) else { return "?" }
return NSLocalizedString(string, tableName: nil, bundle: bundle, value: "", comment: "")
}Also extends to NLLanguage:
postfix func ~ (lang: NLLanguage) -> String {
return Locale.current.localizedString(forIdentifier: lang.rawValue) ?? "ERROR"
}forceTap() — Taps by coordinate when isHittable is unreliable (common with certain SwiftUI controls).
# Unit tests (use the Tests scheme — avoids unsigned Tests.xctest load failures)
xcodebuild -project "Simply Filter SMS.xcodeproj" -scheme "Tests" \
-destination 'platform=iOS Simulator,name=iPhone 18 Pro' test
# App Store screenshots (via Fastlane — configured in fastlane/Fastfile)
fastlane iphone_screenshots # iPhone only
fastlane ipad_screenshots # iPad only (landscape)
fastlane screenshots # Both iPhone + iPad