---
title: "Cut repeated test setup and teardown in CI"
description: "Find the setup and teardown work your tests repeat on every case, such as schema resets, CLI launches, cleanup and password hashing, and do it once."
url: https://starsling.dev/best-practices/github-actions/cut-repeated-test-setup
canonicalUrl: https://starsling.dev/best-practices/github-actions/cut-repeated-test-setup
---

# Cut repeated setup and teardown from your test suite

[GitHub Actions best practices](https://starsling.dev/best-practices/github-actions) / Cut repeated setup and teardown from your test suite

- [How StarSling works](https://starsling.dev/)

Best practice: Caching & Setup. Last updated: 2026-10-27

## Answer asset

### Direct answer

Do expensive fixture work once per suite or per schema, and reset only the state a test reads, so each test spends its time on what it verifies.

### Copyable fix

_Profile CPU-heavy setup in Vitest_

~~~yaml
- name: Profile CPU-heavy setup in Vitest
  run: >-
    pnpm exec vitest run --no-file-parallelism
    --execArgv=--cpu-prof
    --execArgv=--cpu-prof-dir=test-runner-profile
- name: Save CPU profiles
  uses: actions/upload-artifact@v7
  with:
    name: test-runner-profile
    path: test-runner-profile/*.cpuprofile
~~~

### Proof from a real repo

#### better-auth/better-auth#9882: Reuse the schema and reset test data

A schema-keyed reset clears data through the existing Prisma client.

Measured result: **1.09x faster prisma adapter tests** - Test execution step: 301.9 s -> 277.1 s across 10 runs per version (20 jobs), same 1,328 tests.

Source: [better-auth/better-auth#9882](https://github.com/better-auth/better-auth/pull/9882), merged 2026-10-19.

#### better-auth/better-auth#10850: Resolve drizzle-kit once

The suite starts the resolved CLI with the Node executable.

Measured result: **1.16x faster drizzle adapter tests** - Test execution step: 220.0 s -> 190.1 s across 50 runs per version (100 jobs).

Source: [better-auth/better-auth#10850](https://github.com/better-auth/better-auth/pull/10850), merged 2026-10-19.

#### better-auth/better-auth#10762: Keep only pending cleanup rows

Cleanup retains outstanding work and explicit retries.

Measured result: **Up to 5.4x faster** - Mongo adapter Vitest duration: 24.3s -> 4.5s in the PR's benchmark, with 50 runs per arm.

Source: [better-auth/better-auth#10762](https://github.com/better-auth/better-auth/pull/10762), merged 2026-09-03.

#### better-auth/better-auth#10879: Use a fast test-only password hasher

The shared fixture uses a fast default and preserves caller overrides.

Measured result: **~18% faster test job** - Mean test job on Node 22: 359s -> 296s, and on Node 24: 342s -> 280s, across 50 runs per arm (200 jobs total).

Source: [better-auth/better-auth#10879](https://github.com/better-auth/better-auth/pull/10879), merged 2026-08-20.

### Primary sources

- [Vitest test performance profiling](https://vitest.dev/guide/profiling-test-performance)
- [Prisma db push](https://www.prisma.io/docs/cli/db/push)
- [Better Auth #9882](https://github.com/better-auth/better-auth/pull/9882)
- [Better Auth #10850](https://github.com/better-auth/better-auth/pull/10850)
- [Better Auth #10762](https://github.com/better-auth/better-auth/pull/10762)
- [Better Auth #10879](https://github.com/better-auth/better-auth/pull/10879)

## Table of contents

- [Do this](#do-this)
- [Avoid this](#avoid-this)
- [Shipped by StarSling](#shipped-by-starsling)
- [Reset data when the schema is already in place](#reset-only-when-the-schema-changed)
- [Resolve a CLI once before repeated launches](#resolve-a-cli-once)
- [Keep only pending rows in cleanup](#forget-rows-once-cleaned)
- [Use fast defaults in shared test fixtures](#fast-test-only-defaults)
- [Why it matters](#why-it-matters)
- [When to use](#when-to-use)
- [Verify on your repo](#verify)
- [More ways to improve GitHub Actions](#speedup-guides)
- [Sources](#sources)

<a id="do-this"></a>

## Do this

Set up shared resources once per suite or schema, reset test data in process, and keep cleanup lists limited to pending work. Use fast test-only defaults for intentionally expensive production operations, and keep real-path tests for the behavior each optimization touches.

_Profile CPU-heavy setup in Vitest_

```yaml
- name: Profile CPU-heavy setup in Vitest
  run: >-
    pnpm exec vitest run --no-file-parallelism
    --execArgv=--cpu-prof
    --execArgv=--cpu-prof-dir=test-runner-profile
- name: Save CPU profiles
  uses: actions/upload-artifact@v7
  with:
    name: test-runner-profile
    path: test-runner-profile/*.cpuprofile
```

<a id="avoid-this"></a>

## Avoid this

Repeated schema resets, package resolution, cleanup and hashing add up across every test group.

_A test step with no timing baseline_

```yaml
- name: Run tests
  run: pnpm test:run
  # Changing fixture code without timing the full suite hides the effect.
```

<a id="shipped-by-starsling"></a>

## Shipped by StarSling

Four merged Better Auth changes remove repeated work inside the test suite. Each card names its measured scope: test execution step, Vitest duration, or test job duration.

### 1.09x faster prisma adapter tests

better-auth/better-auth#9882: Reuse the schema and reset test data
Test execution step: 301.9 s -> 277.1 s across 10 runs per version (20 jobs), same 1,328 tests.

A schema-keyed reset clears data through the existing Prisma client.

Diff: Repeated schema pushes become in-process data resets when the schema matches.

Source: [better-auth/better-auth#9882](https://github.com/better-auth/better-auth/pull/9882), merged 2026-10-19.
### Customer case study

[How Better Auth got 2x faster E2E tests and cut 20,000 CI minutes a month with StarSling](https://starsling.dev/customers/better-auth)

Per-job E2E from 2m 22s down to 1m 04s. Agent-native CI for the auth library every team's PR queue gates on.


### 1.16x faster drizzle adapter tests

better-auth/better-auth#10850: Resolve drizzle-kit once
Test execution step: 220.0 s -> 190.1 s across 50 runs per version (100 jobs).

The suite starts the resolved CLI with the Node executable.

Diff: All 407 pushes keep their options while each launch skips repeated package resolution.

Source: [better-auth/better-auth#10850](https://github.com/better-auth/better-auth/pull/10850), merged 2026-10-19.

### Up to 5.4x faster

better-auth/better-auth#10762: Keep only pending cleanup rows
Mongo adapter Vitest duration: 24.3s -> 4.5s in the PR's benchmark, with 50 runs per arm.

Cleanup retains outstanding work and explicit retries.

Diff: Processed rows leave the cleanup list after each pass.

Source: [better-auth/better-auth#10762](https://github.com/better-auth/better-auth/pull/10762), merged 2026-09-03.

### ~18% faster test job

better-auth/better-auth#10879: Use a fast test-only password hasher
Mean test job on Node 22: 359s -> 296s, and on Node 24: 342s -> 280s, across 50 runs per arm (200 jobs total).

The shared fixture uses a fast default and preserves caller overrides.

Diff: The reported test-job metric sums the job's steps; production hashing stays unchanged.

Source: [better-auth/better-auth#10879](https://github.com/better-auth/better-auth/pull/10879), merged 2026-08-20.

<a id="reset-only-when-the-schema-changed"></a>

## Reset data when the schema is already in place

[Better Auth #9882](https://github.com/better-auth/better-auth/pull/9882/files) records the last schema pushed per database dialect. When it matches, the existing Prisma client clears table data in process: PostgreSQL uses `TRUNCATE ... RESTART IDENTITY CASCADE`, and MySQL uses `DELETE` in one interactive transaction so session settings stay on the same connection. A changed schema or an unsuccessful reset takes the full `prisma db push --force-reset` path. SQLite keeps its original push path. Preserve test isolation, and keep coverage of the real schema-push path.

### Prompt for your agent

```text
Fetch OPT85 "Tests Run in Many Processes Instead of One" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against it yourself. Find the slowest test job gating the merge and read its step timings over the last 20 runs of its workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run). It applies when that suite's time goes to starting processes: tests that launch a subprocess, interpreter or CLI for each case, or a Rust crate with many integration-test binaries under tests/, each linked and started on its own. Measure it before changing anything: time one case through a subprocess against the same check called in-process, or count the test binaries and time the suite. If it applies, follow the entry's recipe: call the code in-process wherever the test does not exist to verify process behavior, or merge the integration tests into a single test binary, keeping every test case and assertion. Keep at least one test that runs the real process end to end. Report the suite's runtime before and after on the same machine. If nothing qualifies, tell me in one line why. Show me the diff and open a PR rather than applying it blindly.
```

<a id="resolve-a-cli-once"></a>

## Resolve a CLI once before repeated launches

[Better Auth #10850](https://github.com/better-auth/better-auth/pull/10850/files) resolves the drizzle-kit CLI once and starts it with `process.execPath`. The adapter suite still runs all 407 schema pushes, with the same command options and database effects. Each launch becomes cheaper because it starts the resolved executable directly. Measure package resolution separately from the schema work to choose this fix.

<a id="forget-rows-once-cleaned"></a>

## Keep only pending rows in cleanup

[Better Auth #10762](https://github.com/better-auth/better-auth/pull/10762/files) makes cleanup forget processed rows. Previously, each teardown revisited the rows created by earlier tests; 101,366 of 103,554 deletes targeted rows already gone. Retain rows whose cleanup throws or explicitly requests a retry, following the helper's result contract. Check successive cleanup passes as well as the final database state.

<a id="fast-test-only-defaults"></a>

## Use fast defaults in shared test fixtures

[Better Auth #10879](https://github.com/better-auth/better-auth/pull/10879/files) gives the shared fixture a fast salted SHA-256 hash and verify pair. Caller-supplied implementations and normalization stay intact, and production keeps its existing hasher. Keep the production hasher explicitly enabled in tests that verify stored-hash formats, upgrades, compatibility or security behavior. Use the faster default for tests whose subject is the surrounding application behavior.

<a id="why-it-matters"></a>

## Why it matters

A small setup cost becomes a large part of runtime when hundreds of test groups repeat it. Profile the suite before adding runners: reducing repeated work helps every run, including local development. Use the [slow-test diagnosis](https://starsling.dev/github-actions/problems/slow-tests) to locate the cost, and see the [Better Auth case study](https://starsling.dev/customers/better-auth) for the wider CI work.

<a id="when-to-use"></a>

## When to use

**Use it when:** Use this approach when profiling shows shared fixtures, schema resets, CLI startup or teardown consuming a meaningful share of test time. Start with the slowest test job gating the merge.

**Be careful when:** Keep per-test setup when a test verifies initialization, migration, process behavior or isolation itself. Keep production cryptography in security and compatibility tests, and use separate database state for concurrent workers.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

Diagnose why the test job in this repo's GitHub Actions workflow runs much slower than the same suite does locally. Do not change anything yet. First, open the workflow YAML under .github/workflows/ and identify the test job: check whether it uses a strategy.matrix with a --shard flag (Playwright, vitest), and whether it's the single longest job in the workflow's run history. Second, check whether the test job's install step has a matching caching action (actions/cache, or setup-node/setup-python/setup-go with cache: true) for its package manager. Third, profile shared fixture setup and search test helpers for production-strength password hashing or other deliberately expensive cryptography; if it dominates, propose a fast test-only default while preserving normalization and explicit overrides, and never change the production hasher. Keep the production implementation explicitly enabled in tests that verify stored-hash formats, upgrades, compatibility, or security behavior. Fourth, inspect teardown bookkeeping for cleanup passes that revisit successfully deleted IDs; propose removing IDs only after confirmed successful cleanup and retaining every unsuccessful or deferred cleanup according to the helper's result contract. Better Auth #10762 retains thrown failures and explicit `retry` results. Fifth, if this is a monorepo (turbo.json or nx.json present), check whether the test command actually scopes to the diff against the merge base rather than running everything. Sixth, grep the test source (packages/, e2e/, tests/) for fixed-duration waits like waitForTimeout, cy.wait(1000+), or raw sleep() calls, and estimate their total contribution to run time. Report back which causes you found evidence for, ranked by likely impact, before proposing any fix.

Prefer to check by hand:

- For the Vitest example, download the CPU profiles and open them in Chrome DevTools or VS Code to locate fixture and hook CPU work such as hashing. Add wall-clock timers around database calls and subprocess launches to measure their elapsed time. Keep profiling and concurrency settings identical in before-and-after runs.
- Record setup, test-body and teardown timings before editing the shared helper. Count subprocess launches and cleanup operations across successive test groups.
- Run both versions on the same runner type with the same test command, inputs and concurrency. Repeat the measurements and compare the test execution step.
- Confirm the same test files, cases and assertions run before and after. Check that schema changes, concurrent workers, cleanup retries and caller overrides preserve their behavior.
- Keep end-to-end coverage of the real CLI and production hasher where those behaviors are under test.

OPT85 covers the process-startup work in the schema-reset example: Better Auth #9882 replaces repeated CLI launches with an in-process reset.

## Go further

- [Install the ci-speedup skill](https://starsling.dev/skills/ci-speedup) to audit your whole repo against all 91 ci-speedup patterns and get a prioritized fix for every opportunity. Open source, runs locally.

<a id="related-problems"></a>

## Related problems

Diagnosis pages for symptoms that show up next to this practice, each with its triage steps and likely causes.

- [Why your test job is slower in CI than it is locally](https://starsling.dev/github-actions/problems/slow-tests)

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [Shard tests across parallel jobs in GitHub Actions](https://starsling.dev/best-practices/github-actions/shard-tests)
- [Replace fixed CI sleeps with bounded readiness polling](https://starsling.dev/best-practices/github-actions/replace-fixed-sleeps-with-polling)
- [Wait for container healthchecks instead of sleeping](https://starsling.dev/best-practices/github-actions/wait-for-container-healthchecks)
- [GitHub Actions cache: dependencies, keys, and cache hits](https://starsling.dev/best-practices/github-actions/cache-dependencies)
- [All GitHub Actions best practices](https://starsling.dev/best-practices/github-actions)

<a id="speedup-guides"></a>

## More ways to improve GitHub Actions

If you do not know why a run is slow yet, start with the diagnostic guide. [Let an agent find which of these applies: /ci-speedup](/skills/ci-speedup). If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement. To find exploitable workflow paths before attackers do, ci-secure checks the ten critical GitHub Actions attack vectors and lets you choose which fixes to apply.

- [GitHub Actions too slow](https://starsling.dev/github-actions-too-slow)
- [Fast GitHub Actions](https://starsling.dev/fast-github-actions)
- [GitHub Actions alternatives](https://starsling.dev/github-actions-alternatives)
- [GitHub Actions pricing](https://starsling.dev/github-actions-pricing)
- [Self-hosted GitHub Actions runners](https://starsling.dev/self-hosted-github-runners)
- [GitHub Actions CI/CD pipeline](https://starsling.dev/github-actions-ci-cd)
- [Levels of CI: every way to make GitHub Actions cheaper and faster](https://starsling.dev/blog/levels-of-ci)
- [Docker CI on GitHub Actions](https://starsling.dev/ci/docker)
- [Install the free /ci-score skill to improve your GitHub Actions setup](https://starsling.dev/skills/ci-score)
- [Install the /ci-secure skill to close critical attack vectors in GitHub Actions](https://starsling.dev/skills/ci-secure)
- [Browse GitHub Actions agent skills](https://starsling.dev/skills)

<a id="faq"></a>

## FAQ

### How do I find repeated test setup?

Profile shared setup and teardown hooks in the slowest suite. Count schema pushes, CLI launches, cleanup queries and password hashes, then compare their total time with test-body execution.

### Can suite-level setup preserve test isolation?

Yes, when each case starts with the state it expects. Share stable schema and process initialization, reset mutable data between cases, and isolate concurrent workers. Exercise schema changes and reset recovery in dedicated tests.

### Should I shard the suite first?

Measure the bottleneck first. Removing repeated fixture work reduces total execution; sharding distributes the remaining work. Apply both when each has a measurable benefit.

<a id="sources"></a>

## Sources

1. [Vitest test performance profiling](https://vitest.dev/guide/profiling-test-performance)
2. [Prisma db push](https://www.prisma.io/docs/cli/db/push)
3. [Better Auth #9882](https://github.com/better-auth/better-auth/pull/9882)
4. [Better Auth #10850](https://github.com/better-auth/better-auth/pull/10850)
5. [Better Auth #10762](https://github.com/better-auth/better-auth/pull/10762)
6. [Better Auth #10879](https://github.com/better-auth/better-auth/pull/10879)
