Automated Test Runs on Pull Requests
Imagine this: you just merged a PR into main, and a minute later production goes down with an error. It turns out the new module overwrote an old endpoint, and no test caught it. This scenario is familiar to many teams. We've been through it and since then have enforced a hard rule: every PR goes through an automated test pipeline. No green light — no merge. Automated test runs on PR are the basic defense against regressions. A developer cannot merge code that breaks existing functionality. This is not a replacement for code review, but a supplement: reviewers focus on logic, not on catching obvious bugs.
According to GitHub Actions documentation, dependency caching cuts installation time from 90 seconds to 5 seconds — 18 times faster. Matrix test execution lets you check different environment versions in parallel, speeding up the overall run by 3–4 times. This saves up to 40 hours per month on debugging. Our experience — over 5 years in CI/CD and 100+ configured pipelines — confirms that automated test runs pay off within the first sprint.
Why Automated Test Runs on PR Are Critical
Without this protection, you risk:
- A broken CI on
main— if a bug gets merged, the entire next sprint goes to fixing it. - Wasted time on code review — reviewers get distracted by errors that tests could catch.
- Manual testing — the less routine, the higher the delivery speed.
Which Tests to Run and Which to Skip
| Test Type | Mandatory? | When to Run | Purpose |
|---|---|---|---|
| Linting | Yes | On every commit | Consistent style, catch potential bugs |
| Unit tests | Yes | On every PR | Check isolated logic |
| Integration tests | Yes (DB/API) | On every PR | Component interaction |
| E2E | No (on demand) | Only for critical scenario changes | Full user path check |
| Security tests | Recommended | Once a day or on release | Find vulnerabilities |
Pipeline Structure
A well-designed pipeline splits into parallel jobs with a fail-fast strategy:
# .github/workflows/pr.yml
name: PR Tests
on:
pull_request:
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel old runs on new push
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint && npm run type-check
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb
How to Set Up Caching to Speed Up the Pipeline
Without caching, npm ci on a cold runner takes 60–90 seconds. With caching — 5–10 seconds. Use the built-in cache in actions/setup-node or explicit via actions/cache:
# Cache node_modules based on package-lock.json
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # Built-in cache in actions/setup-node
# Or explicitly via actions/cache
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Impact of caching on speed:
| Method | Dependency installation time | Total pipeline time |
|---|---|---|
| No cache | 60–90 s | 3–5 min |
| With cache (actions/setup-node) | 5–10 s | 1–2 min |
| Built-in cache + matrix | 5–10 s per job | 1–2 min (parallel) |
Matrix Testing and Supporting Multiple Stacks
If the application must work on multiple Node.js or PHP versions, use a matrix:
strategy:
matrix:
node-version: [18, 20, 22]
fail-fast: false # Run all versions even if one fails
For Laravel projects, use parallel PHPUnit execution:
- name: Run PHPUnit
run: php artisan test --parallel --coverage-clover=coverage.xml
env:
DB_CONNECTION: pgsql
DB_DATABASE: testing
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: coverage.xml
--parallel runs tests in parallel via brianium/paratest. On 200+ tests, it speeds up by 3–4 times.
Status Checks and Branch Protection
In GitHub Settings → Branches → Branch protection rules, add required status checks: lint, unit, integration. Merging into main without these checks is impossible.
Speed Optimization: Path Filtering and Test Splitting
-
Path filtering — run tests only when relevant files change (e.g., via
pathsin GitHub Actions). - Test splitting — distribute tests across multiple runners (GitHub Actions matrix).
-
Only changed modules — Jest
--changedSince, pytest--testpaths.
Goal: pipeline completes within 5 minutes. Slower pipelines cause developers to ignore them.
Process of Setting Up and What’s Included in the Work
- Analyze current tests and project infrastructure.
- Design the pipeline: define jobs, matrices, caching.
- Implement GitHub Actions configuration tailored to your stack.
- Set up branch protection and required status checks.
- Test on a real PR, debug.
- Document the process and hand over to the team.
As a result, you get:
- GitHub Actions configuration with caching and matrix
- Status checks and branch protection rules
- Coverage reports (Codecov, Coveralls)
- Documentation for running tests locally
- Access to our quick-start template for new projects
- Two weeks of post-release support
Timeline and Cost
Basic setup with unit and integration tests for Node.js or PHP takes 1–2 days. Adding coverage and badge — another half day. Final timeline depends on project complexity (number of services, test types). Cost is discussed individually.
Order CI pipeline setup from our engineers — we guarantee a stable pipeline and documentation. Get a consultation on implementation — contact us to discuss your project.







