Automated Unit Test Generation Pipeline for iOS Using LLMs

duolingo · Education · 2026 · source Agentic: yes · promotional bias: low · source-backed · extracted with prompt 0.5.4 (schema v2)

Generated from extraction.json — do not edit by hand. Every claim cites verbatim source evidence; stated means the quote directly asserts the claim, implied means it supports it indirectly.

Problem

Domain: Mobile software engineering automation

Duolingo needed to expand iOS unit test coverage fast enough to keep pace with weekly releases, monthly code growth, and increasing LLM-generated production code.

Constraints:

Architecture

Duolingo built a set of Temporal workflows around Claude Code, GitHub, CI data, and a shared S3 state store. Developers can trigger tests from a PR label, while a backfill workflow repeatedly scores untested files from coverage data and launches parallel child workflows. Each child starts a remote Claude Code session to generate test files, mocks, and fakes. A lifecycle workflow manages generated-test PRs by assigning reviewers, enabling auto-merge, closing stale or conflicted PRs, retrying CI, and invoking an auto-fix workflow for failing builds.

Diagram confidence: explicit (topology described by the source).

flowchart LR
  coverage-data-store[Coverage data store] --> backfill-workflow[Backfill workflow]
  s3-state-store[S3 state store] --> backfill-workflow
  backfill-workflow --> test-generation-child-workflow[Test generation child workflow]
  test-generation-child-workflow --> claude-code-agent[Claude Code agent]
  claude-code-agent --> github-api[GitHub API]
  test-generation-child-workflow --> s3-state-store
  s3-state-store --> pr-lifecycle-workflow[PR lifecycle workflow]
  github-api --> pr-lifecycle-workflow
  pr-lifecycle-workflow --> ci-auto-fix-workflow[CI auto-fix workflow]
  ci-auto-fix-workflow --> github-api
Drag to pan · scroll or use +/− to zoom · reconstructed from the source's described component topology

Components

Component Role Interacts with
Coverage data store Stores Xcode coverage uploaded by CI and used for file selection. Backfill workflow
Backfill workflow Scores Swift files and launches parallel test-generation workflows for high-priority files. Coverage data store, S3 state store, Test generation child workflow
Test generation child workflow Runs a Claude Code session to generate test code, mocks, fakes, and PRs. Claude Code agent, GitHub API, S3 state store
Claude Code agent Reads testing rules and source files, then writes generated iOS test changes. Test generation child workflow, GitHub API
PR lifecycle workflow Acts as an automated project manager for generated-test pull requests. GitHub API, CI auto-fix workflow, S3 state store
CI auto-fix workflow Fetches CI logs and pushes fixes when generated-test PRs fail CI. PR lifecycle workflow, GitHub API
S3 state store Tracks PR history, retry counts, cooldowns, and action history. Backfill workflow, PR lifecycle workflow
GitHub API Hosts labels, draft PRs, CI status, reviewers, and merge operations. Test generation child workflow, PR lifecycle workflow

Data flow

CI uploads coverage to S3. The backfill workflow scores Swift files, filters out active or cooled-down files, and starts child test-generation workflows. Claude Code reads rule files and source code, creates generated-test PRs, and writes PR state to S3. The lifecycle workflow reads that state and GitHub/CI status to assign reviewers, auto-heal failures, close stale PRs, or set cooldowns.

4 supporting quotes
“The overall system is composed of multiple Temporal workflows that coordinate through a shared S3 state store and GitHub’s API.”article, stated
“It parses coverage data for every Swift file in the repo and runs each through a scoring system that weighs four signals”article, stated
“For each selected file, the backfill workflow starts a child iOSTestGenerationWorkflow. Each child calls a CodingAgentWorkflow, which kicks off a remote Claude Code session.”article, stated
“the backfill workflow records the resulting PR in the S3 state store so the lifecycle workflow can pick it up.”article, stated

Why this architecture

Durable workflows matched the problem because test generation is not a single model call: file selection, PR creation, CI repair, reviewer assignment, retries, cooldowns, and stale-PR cleanup all need stateful coordination. S3 was chosen over DynamoDB for lightweight shared records. Human review stayed in the path because generated tests still needed owner context and approval.

3 supporting quotes
“Generating PRs is only half the problem. If those PRs sit in a queue, accumulate merge conflicts, or never get reviewed, the pipeline produces noise instead of value.”article, stated
“We considered DynamoDB but settled on a lightweight S3-based state store. Each source file and each PR gets its own JSON record, tracking PR history, retry counts, action history, and cooldown timestamps.”article, stated
“Each one of those 250 PRs was reviewed and approved by a human engineer before merging. The pipeline assigns reviewers using git blame history, so the reviewer has appropriate context on the source file being tested.”article, stated

Models used: Claude Code

Scale

Alternatives considered (roads not taken)

Manual local Claude Code runs

Rejected because: Local validation was useful for tuning, but production scale required automated selection, PR generation, and lifecycle management.

“Before building any automation, we spent a few weeks running Claude Code locally and analyzing every CI result.”article, stated
“The backfill Temporal workflow is the heart of the pipeline. It runs every few hours, and its job is to identify which files need tests the most and kick off parallel test generation jobs.”article, stated

DynamoDB for workflow state

Rejected because: The team chose a lightweight file-record model in S3 for PR and file state.

“We considered DynamoDB but settled on a lightweight S3-based state store. Each source file and each PR gets its own JSON record, tracking PR history, retry counts, action history, and cooldown timestamps.”article, stated

Open PRs without lifecycle automation

Rejected because: Unmanaged PRs would create stale work, merge conflicts, and reviewer noise.

“If those PRs sit in a queue, accumulate merge conflicts, or never get reviewed, the pipeline produces noise instead of value.”article, stated

Linux-only workers for final validation

Rejected because: Linux workers could not compile or lint iOS code, so Duolingo planned Mac workers for local validation.

“Since our Temporal workers run on Linux hosts, the agent can’t compile or lint the code it writes before opening a PR.”article, stated
“We’re upgrading our workflows to run on Mac instances soon, which will let the agent run SwiftLint and validate changes locally”article, stated

Patterns

durable-execution (orchestration)

Temporal workflows coordinate long-running test generation, backfill scheduling, lifecycle management, CI repair, retries, and cooldowns.

“The overall system is composed of multiple Temporal workflows that coordinate through a shared S3 state store and GitHub’s API.”article, stated

tool-use-orchestration (agent-architecture)

Claude Code operates through GitHub, CI logs, rule files, repository cloning, and PR comments rather than isolated generation.

“The agent clones the iOS repo, reads our testing rule files (a 12-step testing process plus patterns for mock and fake generation)”article, stated

human-in-the-loop-gate (workflow)

Generated PRs require human engineering review before merging.

“Each one of those 250 PRs was reviewed and approved by a human engineer before merging.”article, stated

eval-driven-development (evaluation)

Early CI outcomes and failure categories drove prompt tuning, selection heuristics, and platform fixes.

“The failures fell into a small number of categories, some of which we could systematically address.”article, stated

observability-tracing (operations)

S3 records track PR history, retry counts, action history, and cooldown timestamps for each generated PR/source file.

“Each source file and each PR gets its own JSON record, tracking PR history, retry counts, action history, and cooldown timestamps.”article, stated

Tradeoffs

Use autonomous backfill plus PR lifecycle automation

Chose scheduled generation and lifecycle workflows over manual one-off Claude Code runs.

Rationale: Manual experimentation informed the system, but ongoing coverage improvement required repeatable selection, generated PRs, and automatic queue management.

“Before building any automation, we spent a few weeks running Claude Code locally and analyzing every CI result.”article, stated
“The backfill Temporal workflow is the heart of the pipeline. It runs every few hours”article, stated

Use S3 as shared state

Chose JSON records in S3 over DynamoDB.

Rationale: The pipeline needed lightweight shared records more than database complexity.

“We considered DynamoDB but settled on a lightweight S3-based state store.”article, stated

Accept human review as the merge gate

Chose generated PRs reviewed by human engineers over fully automatic merge of model-written tests.

Rationale: Human review preserves ownership and context while the automation handles generation and lifecycle overhead.

“Each one of those 250 PRs was reviewed and approved by a human engineer before merging.”article, stated
“The pipeline assigns reviewers using git blame history, so the reviewer has appropriate context on the source file being tested.”article, stated

Gotchas (what bit them)

Generated PRs create noise if their lifecycle is unmanaged.

Root cause: PRs can sit in queues, accumulate conflicts, or miss review assignment.

Resolution: Duolingo added an hourly lifecycle workflow that routes PRs by CI, staleness, review, and retry state.

“If those PRs sit in a queue, accumulate merge conflicts, or never get reviewed, the pipeline produces noise instead of value.”article, stated
“The PR lifecycle workflow runs on a schedule and acts as an automated project manager for every open generated-test PR.”article, stated

Linux execution prevented local iOS validation before opening PRs.

Root cause: Temporal workers ran on Linux hosts while iOS lint/compile validation requires Mac capabilities.

Resolution: Duolingo planned Mac instances for SwiftLint and local validation.

“Since the Temporal workers are Linux machines, the agent has to deal with the constraint of not being able to build or compile the code it writes.”article, stated
“We’re upgrading our workflows to run on Mac instances soon, which will let the agent run SwiftLint and validate changes locally”article, stated

Auto-fix agents can edit outside their intended boundary.

Root cause: A CI auto-fix agent modified production source files inside a test PR.

Resolution: The team is adding stricter file-change guardrails.

“We had an incident where the CI auto-fix agent modified production source files in a test PR instead of limiting changes to test files.”article, stated
“We’re actively adding guardrails to enforce stricter rules about what files the agent can and can’t touch.”article, stated

Review throughput became the next bottleneck after code generation.

Root cause: The pipeline can create PRs faster than reviewers can absorb them.

Resolution: Future work focuses on generated PR quality and a second reviewer agent before human review.

“The real bottleneck is review: The pipeline can produce PRs faster than the existing review process can absorb them”article, stated
“adding a second reviewer agent that verifies each generated test before it reaches a human reviewer.”article, stated

Evaluation

Duolingo evaluated the pipeline through early CI outcome batches, production PR merge metrics, coverage change, first-attempt CI pass rate, and failure categorization.

Metrics: merged PRs · lines of test code · test functions · classes tested · coverage lift · first-attempt CI pass rate · failure rate · generated PRs per day

3 supporting quotes
“We tracked every generated PR’s CI outcome across two early batches”article, stated
“76% of merged PRs passed CI on their first attempt.”article, stated
“our most recent batch of PRs saw a 13.6% failure rate, with the vast majority caused by SwiftLint violations.”article, stated

Outcomes

The article reports a production system running every few hours and generating roughly 20 reviewed PRs per day.

3 supporting quotes
“Over 17 weeks, we generated and merged 250 test PRs, adding ~85K lines of test code and 4,460 test functions”article, stated
“Overall MVVM coverage climbed 240%, from 9% to 30%.”article, stated
“By April, the pipeline was running fully autonomously.”article, stated