How to Set Up Continuous Integration Without Unit Tests

Continuous integration (CI) has become a mainstream practice in modern software development, and for good reason. By automatically building, testing, and validating code changes in a controlled environment, CI helps teams maintain a stable, high-quality codebase and ship updates faster with less risk.

But despite the clear benefits, many organizations still view CI as an all-or-nothing endeavor that requires a major upfront investment in automated testing. A recent survey by CircleCI found that while overall adoption of CI has reached 90%, only about half of teams (52%) are running tests as part of their pipelines.[^1] For the 48% doing CI without tests, that usually means not doing CI at all.

However, the reality is that CI provides tremendous value even with little to no automated testing in place. In this post, we‘ll explore why a lack of tests shouldn‘t prevent you from practicing CI, outline some key non-testing benefits to get excited about, and walk through a simple recipe you can follow to start reaping the rewards. By the end, you‘ll see that a small investment in a basic CI pipeline can yield huge dividends for any project or team.

The Case for Continuous Integration Without Tests

When most developers hear "continuous integration", they immediately picture a robust test suite that thoroughly exercises their application code. While it‘s true that automated testing is CI‘s killer feature, it‘s far from the only reason to adopt the practice. Consider some of the other powerful ways CI can improve your development workflow, with or without tests:

Ensure code builds and compiles

At its core, CI is about continuously verifying that your codebase is in a workable state by building and compiling it on every change. Just getting to this basic level of automation can save countless hours of frustrating debugging and rework.

Imagine a team of 10 developers all working on a shared codebase. Without CI, if even a single commit makes it into the main branch that breaks the build, every other developer will pull down that change and be dead in the water until it‘s fixed. If it takes 30 minutes on average to identify and resolve a broken build, that translates to over 4 hours of lost productivity for the team. Worse still, those interruptions can totally derail focus and flow.

With CI, you can guarantee your main branch is always in a deployable state by automatically compiling every commit in isolation before merging. Instead of pulling in someone else‘s bugs, developers can stay focused on writing code with confidence that it will actually work when integrated. Over time, those wasted hours add up to massive savings.

Catch bugs and coding errors early

After ensuring your code compiles, the next highest-value target for automation is scanning for potential bugs, anti-patterns, and style violations. Most languages have mature "linters" that analyze source code without actually executing it to surface a wide range of issues, including:

  • Syntax and formatting errors
  • Using deprecated or dangerous language features
  • Violations of coding conventions and style guides
  • Security vulnerabilities like SQL injection and cross-site scripting
  • Performance issues like inefficient algorithms or memory leaks

Linters are easy to integrate into CI pipelines and can help identify defects early before they make it into production. A 2020 study by Veracode found that fixing bugs in production costs 6.5 times more than during development.[^2] By shifting quality checks left, teams can limit rework and keep technical debt under control.

Consider a JavaScript application that uses a linter like ESLint to enforce the Airbnb style guide. With over 150 rules covering everything from variable naming to error handling, ESLint can help ensure consistent, readable, and error-free code across the entire codebase. Configuring ESLint to run on every build takes minutes but can save hours of manual code review and bug fixing over the life of a project.

Encourage a culture of code quality

In addition to the technical benefits, CI also has a profound impact on team culture and developer habits. By setting clear and objective standards for code quality, CI nudges developers to be more disciplined and quality-focused in their day-to-day work. It‘s a lot harder to write sloppy code when you know it will be immediately flagged in a build.

Over time, that added bit of rigor and accountability can help instill a true culture of excellence. Developers take more pride in their work, knowing that it meets a high bar before being shared with others. They also spend less time chasing preventable issues, freeing them up to focus on more satisfying and impactful work.

Perhaps most importantly, CI creates a level playing field where all code is evaluated by the same consistent standards. Junior developers can get immediate feedback on their output without the anxiety of human judgment. Senior developers are freed from playing "code cop" and can focus their review time on higher-level concerns. By making code quality a collective responsibility, CI helps foster an environment of transparency, trust, and continuous improvement.

Speed up code reviews and reduce rework

One of the biggest drags on developer productivity is time spent waiting on code reviews. A recent study by Jetbrains found that 60% of developers regularly have to wait more than a day to get feedback on pull requests, leading to increased context switching and frustration.[^3]

With a CI pipeline in place, much of the burden of manual review can be dramatically reduced or eliminated. Instead of scrutinizing every line of code for style or syntax issues, reviewers can focus on the substance and intent of changes. Automated checks also make reviews more asynchronous and less blocking. Developers can keep working on new features while waiting for feedback, knowing that their code will be vetted before merging.

CI also helps reduce the time spent iterating on requested changes after a review. If a reviewer catches an issue that should have been flagged automatically, it‘s easy to add a new check to the pipeline to prevent similar mistakes in the future. Over time, the CI system itself becomes a kind of living documentation of a team‘s quality standards and best practices.

Consider a team that adopts a "push on green" strategy, automatically merging any PR that passes its CI checks. Code reviews can be done after merging, when there‘s less time pressure and risk of blocking others. Reviewers can still leave comments and request changes, but the back-and-forth happens in smaller, more manageable chunks. For a team practicing trunk-based development, this asynchronous, automate-first approach can be a huge boost to throughput and morale.

Getting Started with CI

Hopefully by now you‘re convinced of the value of CI, even without a mature testing practice in place. But how do you actually get started? The specific steps will depend on your chosen tools and platforms, but here‘s a simple playbook you can adapt to begin your CI journey with minimal overhead:

1. Choose a CI platform

The first step in any CI setup is choosing a platform to orchestrate your build pipeline. If you‘re already using a source control management (SCM) tool like GitHub, GitLab, or Bitbucket, you may have access to built-in CI features like GitHub Actions or GitLab CI/CD. These tools offer tight integrations with the rest of the platform and can be a great choice for teams just getting started.

If you need more flexibility or customization, there are also a number of dedicated SaaS CI platforms to choose from, including:

  • CircleCI
  • TravisCI
  • Jenkins
  • Codeship
  • AWS CodeBuild
  • Azure DevOps

When evaluating options, some key criteria to consider include:

  • Compatibility with your tech stack and SCM tools
  • Ease of setup and configuration
  • Availability of pre-built integrations and plugins
  • Pricing model and scalability
  • Support for containerization and parallel builds
  • Quality of documentation and community support

For most use cases, any of the major platforms will work just fine. The more important decision is to just pick one and start experimenting. You can always switch later if your needs change.

2. Configure your build

With a platform in place, it‘s time to configure your first build pipeline. Most CI tools use a configuration file checked into your repository (e.g. .circleci/config.yml or .gitlab-ci.yml) to define your build steps. Here‘s a minimal example for a Node.js project using CircleCI:

version: 2.1
jobs:
  build:
    docker:
      - image: cimg/node:lts
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: npm ci
      - run:
          name: Run linter
          command: npm run lint
      - run:  
          name: Compile code
          command: npm run build

This config tells CircleCI to spin up a Node.js environment, install dependencies, run a linter, and compile the code on every commit. You can customize the steps to fit your own project‘s needs, but the basic pattern of install > lint > compile is a solid foundation for any CI pipeline.

graph LR
A([Commit]) --> B[Install] --> C[Lint] --> D[Compile] --> E([Merge])

3. Add status checks

With your build pipeline humming along, the next step is to enforce passing builds as a requirement for merging code. All major SCM platforms support "status checks" that block pull requests from being merged until certain conditions are met, like a green build.

Here‘s how to set up status checks for a GitHub repository:

  1. Go to your repo‘s "Settings" tab and click "Branches"
  2. Click "Add rule" to create a new branch protection rule
  3. Enter a branch name pattern (e.g. "main") and enable "Require status checks to pass before merging"
  4. Select your CI build from the list of available checks
  5. Optionally enable "Require branches to be up to date before merging" to ensure PRs are always rebased on the latest changes

Now any time a developer opens a pull request, GitHub will display the current status of your CI build and prevent merging if it fails. This simple guardrail dramatically reduces the chances of broken code making it into your main branch.

GitHub status checks

4. Iterate and expand

Congratulations, you now have a working CI pipeline! But don‘t stop there. A healthy CI setup is a living, evolving thing that should grow with your team and codebase. As new tools and best practices emerge, continuously update your config to take advantage of them.

Here are a few ways you might expand your pipeline over time:

  • Add more linters and static analyzers to catch a wider range of issues
  • Gradually introduce unit, integration, and end-to-end tests as time allows
  • Run builds in multiple environments (e.g. different Node versions) to ensure compatibility
  • Implement code coverage thresholds to maintain a minimum level of test quality
  • Automate dependency updates and security scans to stay on top of vulnerabilities
  • Set up notifications for build failures and status changes
  • Experiment with more advanced practices like continuous deployment and infrastructure-as-code

Remember, the goal of CI is not to achieve perfection, but to make consistent, incremental improvements to your development process. By starting with a strong foundation and iterating often, you can reap the benefits of CI at any scale.

Case Study: CI at Acme Co

To illustrate the power of continuous integration in action, let‘s take a look at how one fictional company, Acme Co, transformed their development process with a simple CI setup.

Acme Co is a mid-size software company building web applications for the retail industry. When they first started out, the engineering team was small and scrappy, shipping code directly to production without much process or automation in place. As the company grew, however, this cowboy coding approach started to break down.

Code quality suffered as different teams stepped on each other‘s toes. Bugs and regressions slipped through the cracks, causing endless fire drills and late nights. Deployments became a high-stakes affair, with all hands on deck to manually test and monitor each release. Team morale tanked as developers spent more time fighting fires than shipping features.

Realizing they needed to make a change, Acme‘s engineering leadership decided to invest in a basic CI pipeline. They started by setting up CircleCI to compile their JavaScript code and run a few linters on every pull request. They also configured GitHub to require a passing build before merging any changes to their main branch.

At first, the new checks caused some friction as developers had to adjust to a more disciplined workflow. But over time, the benefits became clear. The linters caught countless bugs and style issues before they made it into the codebase. Automated builds gave developers fast feedback on their changes, reducing the need for manual testing. And with the main branch protected by status checks, the team could finally ship new releases with confidence.

Encouraged by these early wins, Acme doubled down on their CI efforts. They started writing unit tests for their most critical code paths and configured CircleCI to run them on every build. They also set up automated dependency scanning and security audits to stay on top of potential vulnerabilities. As the test suite grew, they even began experimenting with continuous deployment for certain low-risk projects.

Today, Acme‘s CI pipeline is the backbone of their development process. Builds run hundreds of times per day, catching bugs and ensuring a high level of quality across the organization. Developers can focus on writing code instead of putting out fires. And the business can ship new features and fixes to customers faster than ever before.

While there‘s still plenty of room for improvement, Acme‘s story shows the huge impact that even a simple CI setup can have. By starting small and iterating often, any team can begin reaping the benefits of continuous integration, no matter their size or maturity level.

Conclusion

For too long, continuous integration has been synonymous with automated testing in the minds of many developers. While tests are certainly CI‘s killer feature, they are far from the only reason to adopt the practice. As we‘ve seen, CI can provide immense value to any project, with or without a single test in place.

By automatically compiling, linting, and validating code on every change, CI helps teams enforce quality standards, catch bugs early, and protect their main branch from breaking changes. More than just a technical tool, CI also encourages a culture of discipline, transparency, and continuous improvement that benefits developers at all levels.

Best of all, setting up a basic CI pipeline is easier than ever thanks to the wealth of powerful platforms and integrations available today. With just a few lines of configuration, any project can start reaping the benefits of CI in a matter of minutes. And as a team‘s needs grow, their pipeline can evolve right alongside them.

So if you‘re still waiting to adopt CI until you have the perfect test suite in place, I encourage you to reconsider. Start small, experiment often, and enjoy the peace of mind that comes with knowing your code is always in a deployable state. Your future self (and your teammates) will thank you.

[^1]: CircleCI. (2022). 2022 State of Software Delivery. https://circleci.com/resources/2022-state-of-software-delivery/
[^2]: Veracode. (2020). The Cost of Poor Software Quality in the US: A 2020 Report. https://www.veracode.com/sites/default/files/pdf/resources/ipapers/cost-of-poor-software-quality-in-us-2020-report/index.html
[^3]: Jetbrains. (2021). The State of Developer Ecosystem 2021. https://www.jetbrains.com/lp/devecosystem-2021/

Similar Posts