How to Write Integration Tests for Palmier Pro: A Swift Testing Guide

Write integration tests for Palmier Pro by combining ImageVideoGenerator fixtures, MediaManifest construction, and ExportService calls to verify end-to-end video export functionality on macOS.

Palmier Pro’s video editing pipeline relies on tight integration between timeline resolution, media handling, and AVFoundation-based export. Writing robust integration tests ensures that these subsystems cooperate correctly when processing real video assets. This guide walks through the architecture, implementation patterns, and concrete code examples found in the palmier-io/palmier-pro repository.

Understanding the Integration Test Architecture

Integration tests in Palmier Pro exercise four distinct layers in a single asynchronous run. Unlike unit tests that mock AVAssetExportSession, these tests use real video generation and file I/O to catch codec-specific edge cases and export corruption issues.

Media Fixture Generation

The test suite uses ImageVideoGenerator.blackVideo(size:) to create deterministic video assets on-the-fly. This async method returns a temporary URL to a generated black-screen clip, providing a consistent input for the export pipeline without bundling large binary files in the repository.

Project Model and Timeline Resolution

Tests construct a MediaManifest to register fixture assets with unique IDs, then instantiate a MediaResolver to map those IDs to filesystem URLs. The timeline model (Timeline, Clip) describes the sequence of media, while timeline.width and timeline.height define the render canvas dimensions.

Export Service Orchestration

ExportService.export(timeline:resolver:format:resolution:outputURL:) coordinates the conversion of the timeline into an AV composition, applies text layers, and executes an AVAssetExportSession. The service exposes public state (progress, error) for test assertions.

Verification with AVFoundation

After export completion, tests load the output file using AVURLAsset(url:) and async load(_:) calls to verify duration, track presence, and codec compliance. This validates that the exported file is playable and structurally sound, not merely that the export method returned without error.

Step-by-Step Blueprint for Integration Tests

Follow this sequence when adding new integration tests to Tests/PalmierProTests/Export/:

  1. Generate a fixture – Call ImageVideoGenerator.blackVideo(size:) with your target render dimensions (e.g., 320 × 180).
  2. Build a manifest – Create a MediaManifest and append a MediaManifestEntry pointing to the fixture’s absolute path.
  3. Create a resolver – Instantiate MediaResolver with closures returning the manifest and an optional project URL.
  4. Construct the timeline – Use helper functions from Tests/PalmierProTests/Fixtures.swift to create Clip objects and assemble a Timeline with matching width/height.
  5. Set the output destination – Generate a unique temporary URL in NSTemporaryDirectory() with a .mp4 extension.
  6. Execute the export – Await ExportService().export(...) with your chosen ExportFormat (e.g., .h264) and ExportResolution (e.g., .r720p).
  7. Assert service state – Verify svc.error == nil, svc.progress == 1.0, and that the output file exists.
  8. Validate the asset – Load the exported file with AVURLAsset and check that duration.seconds matches expectations and video tracks are present.
  9. Clean up – Remove the temporary file in a defer block to prevent disk pollution.

Complete Code Example: Export Round-Trip Test

The reference implementation in ExportServiceRoundTripTests.swift demonstrates a full integration test using the Swift Testing framework (@Suite and @Test attributes):

// Tests/PalmierProTests/Export/ExportServiceRoundTripTests.swift
@Suite("ExportService — round‑trip")
@MainActor
struct ExportServiceRoundTripTests {

    @Test func h264ExportProducesPlayableMp4ContainingVideoTrack() async throws {
        // 1️⃣  Create a 320 × 180 black‑video fixture.
        let renderSize = CGSize(width: 320, height: 180)
        let blackURL   = try await ImageVideoGenerator.blackVideo(size: renderSize)

        // 2️⃣  Register the fixture in a manifest.
        let mediaRef = "black-fixture"
        var manifest = MediaManifest()
        manifest.entries = [
            MediaManifestEntry(id: mediaRef, name: "black",
                              type: .video,
                              source: .external(absolutePath: blackURL.path),
                              duration: 5.0)
        ]

        // 3️⃣  Resolve URLs via a resolver.
        let resolver = MediaResolver(manifest: { manifest }, projectURL: { nil })

        // 4️⃣  Build a one‑second clip on a timeline.
        let clip = Fixtures.clip(id: "c1", mediaRef: mediaRef, start: 0, duration: 30)
        var timeline = Fixtures.timeline(tracks: [Fixtures.videoTrack(clips: [clip])])
        timeline.width  = Int(renderSize.width)
        timeline.height = Int(renderSize.height)

        // 5️⃣  Export to a temporary file.
        let outURL = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("export-\(UUID().uuidString).mp4")
        defer { try? FileManager.default.removeItem(at: outURL) }

        let svc = ExportService()
        await svc.export(timeline: timeline,
                        resolver: resolver,
                        format: .h264,
                        resolution: .r720p,
                        outputURL: outURL)

        // 6️⃣  Check ExportService state.
        #expect(svc.error == nil, "export reported error: \(svc.error ?? "")")
        #expect(svc.progress == 1.0)
        #expect(FileManager.default.fileExists(atPath: outURL.path))

        // 7️⃣  Load the exported file and verify it is a real video.
        let asset    = AVURLAsset(url: outURL)
        let duration = try await asset.load(.duration)
        #expect(duration.seconds > 0)
        #expect(abs(duration.seconds - 1.0) < 0.5,
                "expected ~1 s exported, got \(duration.seconds)s")

        let videoTracks = try await asset.loadTracks(withMediaType: .video)
        #expect(!videoTracks.isEmpty, "exported file has no video tracks")
    }
}

This test verifies that the entire pipeline—from media generation through ExportService to final file output—produces a valid video file with the expected duration and track structure.

Testing Advanced Scenarios

Transform Keyframes and Animation

Test complex timeline behavior by attaching KeyframeTrack objects to clips. This example from ExportServiceRoundTripTests.swift validates that keyframes starting at clip offset zero do not crash the export:

@Test func exportSurvivesTransformKeyframeAtClipOffsetZero() async throws {
    // Generate fixture and build manifest (steps omitted for brevity)
    
    // Apply scale and position keyframes
    var clip = Fixtures.clip(id: "c1", mediaRef: mediaRef, start: 0, duration: 30)
    clip.scaleTrack = KeyframeTrack(keyframes: [
        Keyframe(frame: 0, value: AnimPair(a: 1.0, b: 1.0), interpolationOut: .linear),
        Keyframe(frame: 30, value: AnimPair(a: 1.08, b: 1.08), interpolationOut: .linear),
    ])
    clip.positionTrack = KeyframeTrack(keyframes: [
        Keyframe(frame: 0, value: AnimPair(a: 0, b: 0), interpolationOut: .linear),
        Keyframe(frame: 30, value: AnimPair(a: -0.04, b: 0), interpolationOut: .linear),
    ])
    
    // Proceed with timeline assembly and export...
}

Export Resolution Logic

Verify that ExportResolution correctly calculates output dimensions for different aspect ratios in ExportResolutionTests.swift:

// Tests/PalmierProTests/Export/ExportResolutionTests.swift
@Suite("ExportResolution.renderSize")
struct ExportResolutionTests {

    @Test func landscape720pDownscalesShortSideTo720() {
        let size = ExportResolution.r720p.renderSize(for: CGSize(width: 1920, height: 1080))
        #expect(size == CGSize(width: 1280, height: 720))
    }
}

Key Source Files for Integration Testing

When writing integration tests for Palmier Pro, reference these core files:

Summary

Writing integration tests for Palmier Pro requires coordinating media generation, timeline modeling, and AVFoundation export verification in a single async test function. By using ImageVideoGenerator for deterministic fixtures and AVURLAsset for post-export validation, you catch real-world bugs that unit tests with mocked dependencies cannot detect.

  • Use the Swift Testing framework’s @Suite and @Test attributes with @MainActor for UI-related tests
  • Always verify exported files with AVURLAsset to confirm duration and track presence
  • Leverage Fixtures.swift helpers to reduce boilerplate when building timelines
  • Test edge cases like keyframe offsets and resolution downscaling to prevent regression

Frequently Asked Questions

What testing framework does Palmier Pro use for integration tests?

Palmier Pro uses the Swift 5.9-compatible Testing framework, utilizing @Suite and @Test attributes instead of XCTest. This framework supports async/await syntax natively, making it ideal for testing ExportService methods that call AVAssetExportSession asynchronously.

Why generate video fixtures instead of using bundled test files?

The ImageVideoGenerator.blackVideo(size:) method creates deterministic assets on-demand, keeping the repository size small while ensuring tests run with fresh, uncorrupted files. Generated fixtures also allow testing specific resolutions (e.g., 320 × 180) without maintaining multiple binary assets.

How do I test different export formats like HEVC or ProRes?

Pass the desired format to the ExportService.export(...) method’s format parameter. For example, use format: .h265 instead of .h264, then verify codec-specific properties by inspecting AVAssetTrack format descriptions after export completion.

Should integration tests clean up temporary files?

Yes. Always wrap temporary file removal in a defer block immediately after creating the output URL, as shown in the ExportServiceRoundTripTests.swift example. This prevents accumulation of test artifacts in NSTemporaryDirectory() across test runs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →