Why Your Build Routine Needs a Testing Pipeline
Automated testing can give you much confidence in merging your changes, especially in extensive refactoring or when working with teammates. Including testing as part of your build routine gets the most value out of it — even for small side projects, you don't need to be a DevOps engineer to set it up.
Consider the familiar scenario: You're approaching a deadline, performing a complex refactoring with plenty of CSS changes, and working on the last steps during your bus ride. Your local tests fail every time, and your stress level rises. This is precisely the kind of situation a well-designed testing pipeline can prevent.
Essential Terminology
When researching testing pipelines, you'll encounter the terms "CI/CD" early on. It's short for "Continuous Integration, Continuous Delivery" and "Continuous Deployment" — a software distribution method used by development teams to deploy code changes more frequently and reliably. CI/CD relies heavily on automation.
- Continuous Integration
Automation measures that implement small, regular code changes and merge them into a shared repository. This includes building and testing your code. - Continuous Delivery
Refers to code that has already been tested, ready for operations teams to deploy to a live production environment. This last step may be manual. - Continuous Deployment
The fully automated release process that moves developer changes from the repository directly to production.
These processes aim to keep a product releasable at any time, with the confidence of a continuously monitored, tested, and deployed application. Pipelines are the "tubes" that transport software through these stages — in this guide, we focus exclusively on the testing aspect of that pipeline.
Types of Tests to Consider
Before building a testing pipeline, it's important to understand the available testing types:
- Unit testing individually and independently tests the smallest testable parts or units of an application.
- Integration testing focuses on the interaction between components or systems, checking how units work together.
- End-to-End (E2E) testing simulates actual user interactions by computer, including as many functional areas and parts of the technology stack as possible.
- Visual testing checks the visible output of an application and compares it to expected results — finding "visual bugs" distinct from functional bugs.
- Static analysis isn't strictly testing, but it debugs your code without running the program and detects code style issues, working like a spelling correction that prevents many bugs.
To feel confident merging a massive refactoring, consider using all these testing types. But head-starting with everything leads to frustration quickly — you need a strategy to determine where to start and how many tests of each type are reasonable.
From Test Pyramid to Testing Trophy
The test automation pyramid, introduced by Mike Cohn in "Succeeding with Agile" and further developed as the "Practical Test Pyramid" by Martin Fowler, provides one answer. It consists of three layers:
- Unit — on the base layer since these tests are fast to execute and simple to maintain due to their isolation. See an example unit test testing a small product.
- Integration — in the middle, still acceptable in execution speed while offering confidence closer to the user. API tests and component tests fall here.
- E2E tests (also called UI tests) — simulating a genuine user, these require more time to execute and are more expensive, placing them at the top of the pyramid. See an example E2E test.
However, this metaphor has aged poorly. A significant flaw: static analyses are bypassed in this strategy. Linting and other static analysis tools serve as an integral part of modern pipelines and shouldn't be ignored.
"Write tests. Not too many. Mostly integration." — Guillermo Rauch
This philosophy break down into three parts: always write tests to instill trust in your application; avoid writing tests at random without prioritization; and favor integration tests since confidence increases as you move toward user-facing tests. While E2E tests mimic users most closely, they're much slower and require the full application stack. Integration tests offer the best balance between user trust, execution speed, and maintenance effort.
Kent C. Dodds extended this thinking into a new strategy he calls the "Testing Trophy," which reshapes the pyramid to reflect higher priority on integration tests. The key difference is its test distribution:
- Static analysis plays a vital role — catching typos, type errors, and other bugs through debugging steps alone.
- Unit tests still ensure smallest units are tested, but receive less emphasis than in the pyramid.
- Integration is the main focus, balancing cost and confidence best.
- UI tests (E2E and visual) sit at the top, similar to their position in the pyramid.
The choice of strategy always depends on the project at hand, but the testing trophy works well for most front-end projects. The key takeaway: think through the prioritization and distribution of testing types before planning and implementing your pipeline.
Choosing a Realistic Test Bed
Before a pipeline can be assembled, you need a codebase that represents a typical front-end scenario — one that is small enough to manage by hand yet complex enough to demonstrate value across test layers. A personal portfolio site fits that description well: it is a common project type, small in scope, and easy to inspect or fork.
One such example is the author’s own open-source portfolio, built with Vue.js (version 2) and Nuxt.js. The full implementation is available in the associated GitHub repository, making it possible to follow along with the pipeline construction process step by step.
Shaping the Test Strategy
Given the project’s modest size, the proposed strategy collapses the unit and integration test layers into a single merged tier. This is not merely a convenience for small codebases; several practical arguments support it:
- “Unit” is ambiguous. Front-end developers rarely agree on what constitutes a unit — some mean a single function or class, while others include the entire component in that definition.
- The boundary is blurry. In front-end work especially, drawing a clear line between unit and integration tests is difficult. The DOM is often required to validate behavior, which pushes what might be a unit test into integration territory.
- The tooling overlaps. The same libraries and frameworks can cover both layers, so maintaining separate pipelines for them would duplicate effort without adding clarity.
The resulting concept, which applies the testing trophy model to this smaller project, keeps end-to-end (E2E) tests at the top and uses the merged lower layer to handle everything beneath.
When time is short, it is tempting to rely on local tests only — a scenario made all too familiar by the protagonist of a popular streaming series who runs Cypress locally while racing a deadline. But a reliable pipeline requires more than that; it needs a CI/CD setup that runs the same tests consistently, on every change, without depending on anyone’s local environment.
Choosing the CI/CD Platform
Once the pipeline structure is clear, the next decision is which continuous integration platform to use. Personal experience covers GitLab from daily work and GitHub Actions for side projects, but many other options exist. The choice should always be project-specific, factoring in the technologies and frameworks involved to avoid compatibility issues. Since the portfolio project is a Vue 2 application already hosted on GitHub, GitHub Actions is a natural fit — it only requires the repository itself as a starting point.
GitHub Actions lets you define workflows that run when specific events occur, such as a push to a branch. While this guide focuses on CI/CD, workflows can also automate other repository activities like labeling pull requests. Execution happens on Windows, Linux, or macOS virtual machines. A single workflow will represent the entire pipeline, from static analysis through all UI tests. Each workflow contains one or more jobs — sets of steps run on the same runner. Jobs consist of steps that either execute a simple script or run a reusable action (a complete, custom application).
In practice, a workflow looks like this:
Setting Up the Workflow
To start, create the .github/workflows/ directory and a new file called tests.yml to hold the workflow. The initial setup follows this pattern:
- Name the workflow
Tests CI. - Trigger it on
pushto any remote branch and enable manual runs viaworkflow_dispatch. - Include three jobs:
static-eslint,unit-integration-jest(merging unit and integration tests), andui-cypress(covering E2E and visual regression). - Run all jobs on a Linux virtual machine using
ubuntu-latest.
In the correct YAML syntax, the workflow’s first outline is:
name: Tests CI
on: [push, workflow_dispatch] # On push and manual
jobs:
static-eslint:
runs-on: ubuntu-latest
steps:
# 1 steps
unit-integration-jest:
runs-on: ubuntu-latest
steps:
# 1 step
ui-cypress:
runs-on: ubuntu-latest
steps:
# 2 steps: e2e and visual
Details on workflow syntax are available in the GitHub Actions documentation. The steps are still missing, so the next sections define each job with the tools needed to automate the relevant tests.
Static Analysis
Following the testing trophy approach, the workflow starts with linters and code-style tools. Options include:
- ESLint for JavaScript style enforcement.
- Stylelint for CSS rules.
- Additional tools like Scrutinizer for code complexity analysis.
These tools identify pattern and convention violations. Rule strictness is a matter of preference — what matters is consistent style and catching real error sources, like using == versus ===. For this project, ESLint is the choice due to the heavy JavaScript usage. Install it with:
npm install eslint --save-dev
After installation, create a configuration file named .eslintrc.json. A basic configuration is sufficient for this guide:
{
"extends": [
"eslint:recommended",
]
}
To automate execution, set the lint command as an NPM script in the package.json script section:
"scripts": {
"lint": "eslint --ext .js .",
},
The workflow job then checks out the project with the actions/checkout@v2 action, installs the NPM dependencies, and runs the ESLint script. A failing lint command fails the pipeline automatically:
static-eslint:
runs-on: ubuntu-latest
steps:
# Action to check out my codebase
- uses: actions/checkout@v2
# install NPM dependencies
- run: npm install
# Run lint script
- run: npm run lint
Unit and Integration Testing
The next job handles unit and integration tests. The Jest framework is used here, though alternatives like Cypress component testing or Jasmine are also viable. Jest, an open-source Facebook project, emphasizes simplicity and works with Vue.js, React, Angular, and TypeScript — making it compatible with the portfolio project. Install it from the project root:
npm install --save-dev jest
Writing tests is outside this guide’s scope; the focus is automation. Again, create an NPM script for the Jest test run:
"scripts": {
"test": "jest",
},
The unit-integration-jest job follows the same pattern as the linting one, with two differences: it uses an action to install Node, then executes the Jest script:
unit-integration-jest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Set up node
- name: Run jest
uses: actions/setup-node@v1
with:
node-version: '12'
- run: npm install
# Run jest script
- run: npm test
UI Tests: E2E and Visual Regression
The final job, ui-cypress, combines E2E testing and visual testing using Cypress, though alternatives like NightwatchJS and CodeceptJS exist. Setup is minimal since Cypress provides its own GitHub action, cypress-io/github-action@v2. Several configurations are required: the application must be running (E2E tests need the full stack), the browser must be named, and the web server must be ready before tests execute. The action’s with area holds these settings:
steps:
- name: Checkout
uses: actions/checkout@v2
# Install NPM dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress Run
uses: cypress-io/github-action@v2
with:
browser: chrome
headless: true
# Setup: Nuxt-specific things
build: npm run generate
start: npm run start
wait-on: 'http://localhost:3000'
Adding Visual Testing with Percy
E2E tests verify only what is written, which is limited when refactoring SCSS files across the board. Visual testing catches styling issues that scripted checks miss. A screenshot comparison approach identifies unintended visual changes against a baseline, which is valuable during large CSS refactors. Tools to consider:
- Percy.io by Browserstack, used in this guide.
- Visual Regression Tracker for a fully open-source, self-hosted option.
- Applitools with AI support.
- Chromatic by Storybook.
Percy is chosen for its simple integration with existing Cypress tests. While it is a SaaS tool, it offers open-source components and a free plan. Install the Cypress plugin:
npm install --save-dev @percy/cli @percy/cypress
Import the package in cypress/support/index.js:
import '@percy/cypress';
This enables the snapshot command, which captures screenshots across configurable viewports and browsers:
it('should load home page (visual)', () => {
cy.get('[data-cy=Polaroid]').should('be.visible');
cy.get('[data-cy=FeaturedPosts]').should('be.visible');
// Take a snapshot
cy.percySnapshot('Home page');
});
In the workflow, Percy runs as the job’s second step with npx percy exec -- cypress run. A Percy token, passed via a GitHub secret, connects the tests to the Percy project:
steps:
# Before: Checkout, NPM, and E2E steps
- name: Percy Test
run: npx percy exec -- cypress run
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
The token is necessary because Percy is a SaaS solution that stores screenshots, maintains baselines, and provides an approval workflow for accepting or rejecting visual changes:
Reviewing Workflow Results
After pushing the workflow, results appear in the repository’s “Actions” tab. All workflows are listed there, matching the workflow files. Selecting one — like the “Tests CI” workflow — shows all its jobs. Each job can be opened to inspect its logs:
The same results are visible in pull requests. A typical setup requires all GitHub action checks to pass before any pull request can be merged.
Why CI/CD Testing Matters for Frontend Teams
A well-configured CI/CD pipeline turns even major refactors into low-risk operations. The testing stages within that pipeline continuously monitor the codebase, so errors surface early — ideally before a merge into the main branch. This approach eliminates the all-too-common scramble of fixing local test failures on the way to work, or, worse, discovering real application bugs after deployment.
Setting up this kind of testing routine does not require deep DevOps expertise. With testing frameworks and GitHub Actions, individual developers can apply the same practices to side projects without an elaborate infrastructure setup.
Key Resources for Building a Testing Pipeline
Several guides and tools serve as solid starting points for implementing a frontend testing pipeline:
- GitHub’s own guide on CI/CD explains the underlying concepts in clear, practical terms.
- Ham Vocke’s The Practical Test Pyramid is still the canonical reference for deciding where to place your testing efforts.
- Kent C. Dodds’ series on the testing trophy — including Write tests. Not too many. Mostly integration, The Testing Trophy and Testing Classifications, and Static vs Unit vs Integration vs E2E Testing for Frontend Apps — breaks down the trade-offs of each test type.
- The Cypress real-world app provides working examples you can study and reuse.
Tooling Documentation
The technologies referenced in the pipeline have extensive, current documentation:
- GitHub Actions for workflow automation.
- ESLint for static code analysis and rule enforcement.
- Jest for unit and integration testing in JavaScript projects.
- Cypress for end-to-end testing of user flows.
- Percy for visual regression testing.



