After a user selects an image from the photo library, the app may need to read its orientation metadata, generate a thumbnail, compress the original, and upload it. Videos introduce additional branches for duration, encoding, and file size. Manually testing the flow once on a development machine does not prove that the next build will handle the same inputs correctly. A more reliable approach is to commit media files to the repository as test fixtures, reset an isolated simulator on a cloud Mac, and inject them into the photo library with simctl addmedia.
Define the Boundaries to Test
These tests should focus on the interface between the app and the system photo library rather than automating the Photos app itself. At a minimum, cover these four paths:
- The app can discover the expected number of images and videos.
- After the picker returns, the file types, pixel dimensions, and video durations match expectations.
- Compression or upload tasks use the original assets rather than previews obtained by accident.
- Unsupported formats, oversized files, and permission changes enter clearly defined error paths.
simctl addmedia is suitable for validating read and processing flows in the simulator, but it cannot replace testing camera capture, device-specific encoding, or real-world storage pressure. Document these boundaries in the test specification so that simulator limitations are not mistaken for product defects when failures occur.
The goal of regression testing is not to add as many media files as possible. Each fixture should correspond to a behavior that can be named, repeated, and diagnosed.
Prepare Small, Stable Media Fixtures
Create Tests/Fixtures/Media in the repository and commit only the smallest files required by the tests. File names should describe their intended purpose rather than using names generated automatically by a camera.
| File | Purpose | Primary assertion |
|---|---|---|
portrait.jpg |
Portrait image with orientation metadata | Width and height are correct after orientation is applied |
transparent.png |
Image with an alpha channel | Transparency is preserved after export |
short.mov |
Short video | Duration and media type are correct |
invalid.txt |
Non-media file | The injection step rejects it explicitly |
Also store a SHA-256 manifest to prevent image tools from rewriting fixtures unnoticed:
cd Tests/Fixtures/Media
shasum -a 256 portrait.jpg transparent.png short.mov > SHA256SUMS
shasum -a 256 -c SHA256SUMS
Do not re-encode fixtures while the job is running. Encoders on different system versions may write different metadata, turning an “application behavior changed” issue into an “input changed” issue. If dynamic files are genuinely necessary, record the generation script, its parameters, and the digest of each generated file.
Isolate a Simulator for Each Job
A shared, long-running simulator retains photo library assets, permission records, and background processes. The simplest baseline is to have the orchestration layer provide DEVICE_UDID, then shut down, erase, and boot the simulator at the start of the job before waiting for startup to complete:
set -euo pipefail
: "${DEVICE_UDID:?DEVICE_UDID is required}"
: "${BUNDLE_ID:?BUNDLE_ID is required}"
xcrun simctl shutdown "$DEVICE_UDID" 2>/dev/null || true
xcrun simctl erase "$DEVICE_UDID"
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID" -b
xcrun simctl privacy "$DEVICE_UDID" reset photos "$BUNDLE_ID" || true
xcrun simctl privacy "$DEVICE_UDID" grant photos "$BUNDLE_ID"
Pin the simulator model and OS version in the job configuration as well. Do not specify only name=iPhone and let Xcode choose. Parallel jobs must use different UDIDs; otherwise, one job may run erase while another is still executing tests.
Split permission testing into two groups. The main flow should receive photo library access in advance so it can validate media processing. The denied-permission flow should run revoke separately and assert only the app’s message and recovery entry point. Do not make test success depend on whether a system dialog appears.
Inject Fixtures and Run Targeted Tests
Before injection, verify the file extensions and digests, then pass all supported files to addmedia in a single command:
FIXTURES="$PWD/Tests/Fixtures/Media"
shasum -a 256 -c "$FIXTURES/SHA256SUMS"
xcrun simctl addmedia "$DEVICE_UDID" \
"$FIXTURES/portrait.jpg" \
"$FIXTURES/transparent.png" \
"$FIXTURES/short.mov"
xcodebuild test \
-workspace Demo.xcworkspace \
-scheme Demo \
-destination "platform=iOS Simulator,id=$DEVICE_UDID" \
-only-testing:MediaImportTests \
-resultBundlePath "$PWD/Artifacts/MediaImport.xcresult"
A successful addmedia result means only that the command accepted the files; it does not mean the app can already see the correct assets. The test code should wait until the photo library query returns the expected count, with a finite timeout. Prefer assertions based on media type, pixel dimensions, duration range, and application-generated results. Do not rely on display order in the system photo library because import time and asset creation time may differ.
If the app uploads files, replace the network layer with a local test implementation that records the received MIME type, byte count, and digest. This confirms that the app read the original asset without introducing external network instability into the regression results.
Make Failures Reviewable
A failed job should retain at least three kinds of evidence: the xcresult, fixture digests, and device information for the current run. You can also print the current list of available simulators, but do not collect unrelated contents from user directories.
Common misdiagnoses and their remedies include:
- More assets than expected: The simulator was not erased before the job, or multiple jobs reused the same UDID.
- The test occasionally cannot find the video: The query ran immediately after import without waiting for the photo library to finish updating.
- Image width and height are reversed: The code ignored orientation metadata when reading pixel dimensions.
- Passes locally but fails in CI: The test depends on uncommitted files, default permissions, or an ambiguous destination.
- Upload digest changes: The app re-encoded the asset; record the input digest and upload-body digest separately.
Shut down the simulator when the job finishes. If the current job created the device temporarily, delete it outright. Long-lived devices should also be erased before the next job; do not rely on deleting photo library items one by one. The final checklist can be reduced to these points: fixture digests match, the UDID is used exclusively, the OS version is pinned, permission state is explicit, the test waits for the expected import count, and the result bundle is archived. With these controls in place, media import failures have stable inputs and a reviewable state.
Frequently asked questions
Can simctl addmedia replace photo tests on physical devices?
Not entirely. It is suitable for simulator coverage of discovery, selection, reading, and upload flows, while camera capture, device-specific codecs, and real storage pressure still require device testing.
How do I stop media from accumulating between CI runs?
Use a dedicated simulator for each job, shut it down and erase it before injection, then delete the temporary device after the test instead of sharing a persistent photo library.
Run your next build on a dedicated Apple Silicon physical node
Choose a rental term and node region, review the Mac Mini M4, 16GB RAM, 256GB SSD, and USD amount due, then place your order.