Skill: Snapshot Tests
Guide for creating Snapshot Tests using Point-Free's SnapshotTesting library.
When to use this skill
- •Create snapshot tests for a View
- •Test different view states visually
- •Ensure visual regression prevention
Additional resources
- •For complete implementation examples, see examples.md
Prerequisites
- •
SnapshotTestingdependency in snapshot test targets - •
DSAsyncImagecomponent (replacesAsyncImage) - •
ImageLoaderMockin CoreMocks - •Test image in
Tests/Shared/Resources/
File structure
code
Tests/
├── Snapshots/ # Snapshot tests
│ └── Presentation/
│ └── {Name}/
│ ├── {Name}ViewSnapshotTests.swift
│ └── __Snapshots__/
└── Shared/ # Shared resources
├── Stubs/
│ └── {Name}ViewModelStub.swift
└── Resources/
└── test-avatar.jpg
Key Components
DSAsyncImage
Views must use DSAsyncImage instead of AsyncImage. Uses AsyncImagePhase for handling states:
swift
DSAsyncImage(url: character.imageURL) { phase in
switch phase {
case .success(let image):
image.resizable().scaledToFill()
case .empty:
ProgressView()
case .failure:
Image(systemName: "photo")
@unknown default:
ProgressView()
}
}
ViewModel Protocol
Create a protocol for stub injection:
swift
protocol {Name}ViewModelContract: AnyObject {
var state: {Name}ViewState { get }
func load() async
}
ViewModel Stub
Returns fixed state without logic:
swift
@Observable
final class {Name}ViewModelStub: {Name}ViewModelContract {
var state: {Name}ViewState
init(state: {Name}ViewState) {
self.state = state
}
func load() async { }
}
SnapshotStubs
swift
enum SnapshotStubs {
static var testImage: UIImage? {
guard let path = Bundle.module.path(forResource: "test-avatar", ofType: "jpg") else {
return nil
}
return UIImage(contentsOfFile: path)
}
}
Test Structure
Uses instance variables pattern for cleaner tests:
swift
struct {Name}ViewSnapshotTests {
// MARK: - Properties
private let imageLoader: ImageLoaderMock
// MARK: - Initialization
init() {
UIView.setAnimationsEnabled(false)
imageLoader = ImageLoaderMock(image: SnapshotStubs.testImage)
}
// MARK: - Tests
@Test("Renders loading state correctly")
func loadingState() {
// Given
let viewModel = {Name}ViewModelStub(state: .loading)
// When
let view = NavigationStack {
{Name}View(viewModel: viewModel)
}
.imageLoader(imageLoader)
// Then
assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
}
@Test("Renders loaded state correctly")
func loadedState() {
// Given
let viewModel = {Name}ViewModelStub(state: .loaded({Name}.stub()))
// When
let view = NavigationStack {
{Name}View(viewModel: viewModel)
}
.imageLoader(imageLoader)
// Then
assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
}
}
Benefits of instance variables:
- •
imageLoadercreated once ininit() - •Each test only sets up test-specific state (viewModel)
- •Cleaner
// Givensection
Key Rules
Test Setup
- •Disable animations:
UIView.setAnimationsEnabled(false) - •Create
ImageLoaderMockwith test image - •Inject
.imageLoader(imageLoader)on view
View Configuration
- •Wrap in
NavigationStack - •Use
.iPhone13ProMaxdevice config - •Apply
imageLoadermodifier
Naming
- •Test file:
{Name}ViewSnapshotTests.swift - •Test method:
{stateName}State()(e.g.,loadingState) - •Test description:
@Test("Renders {state} state correctly") - •Snapshots folder:
__Snapshots__/{Name}ViewSnapshotTests/
Running Tests
First Run (Recording)
bash
tuist test {Module}
First run creates references and fails (expected).
Subsequent Runs
bash
tuist test {Module}
Regenerate Snapshots
bash
rm -rf Tests/Snapshots/Presentation/{Name}/__Snapshots__
tuist test {Module} # Run twice
Checklist
Setup (once per feature)
- • Add
SnapshotTestingto snapshotTestDependencies - • Create test image in
Tests/Shared/Resources/ - • Create ViewModel stub in
Tests/Shared/Stubs/ - • Create ViewModel protocol
Per View
- • Use
DSAsyncImagein View - • Create snapshot tests file in
Tests/Snapshots/ - • All
@Testattributes include a description - • Initialize
ImageLoaderMock - • Test each state
- • Use
.iPhone13ProMaxconfig - • Run tests twice (record + verify)