Tests Before Deployment: How I Designed the integration of PHPUnit and Bruno into the CI/CD Pipeline

5 min read 1 views 1 views September 26, 2026
Tests Before Deployment: How I Designed the integration of PHPUnit and Bruno into the CI/CD Pipeline

Hi! In this article, I'll share how I organized the integration of automated tests into the Jenkins pipeline for oninvest-backend.

The task was simple in concept and quite careful in implementation: do not deploy code until PHPUnit and Bruno confirm everything works.

It was framed broadly — "add automated tests before deployment" — with no ready-made recipe. I thought through the entire mechanics from scratch: I proposed the approach with a temporary directory and separate test containers, so that the main project directory stays untouched until tests are green. First on dev environments, with production in mind.

In short: now on every build, the code first goes into a temporary directory, a test environment is brought up, PHPUnit and Bruno are run, and only after success is the code "promoted" into the main directory and deployed. If tests fail, deployment is blocked, but reports are still saved in Jenkins.


1. Why We Needed This

Before tests were introduced, the pipeline was straightforward: build and deploy. Fast, but risky. Especially when changes touched APIs, migrations, or configs. We wanted **fail-fast**: find out something broke before rollout, not after.

What we wanted:

- PHPUnit and Bruno run automatically before deployment.
- Main projectfiles/ directory is untouched until tests pass.
- Bruno runs on the host and hits the test container on port **8081**.
- PHPUnit runs inside the container.
- JUnit reports are always published to Jenkins, even if tests fail.
- There is a way to quickly skip tests via RUN_TESTS, but by default they are enabled.


2. Pipeline Architecture

The pipeline consists of 4 Shell steps:

Git push / Jenkins build
        |
        v
[1] Rsync code
        |
        |-- RUN_TESTS=true  -> projectfiles-test/
        |-- RUN_TESTS=false -> projectfiles/
        v
[2] build-test-runner.sh
    docker build + up + DB migrations
        |
        v
[3] run-tests.sh
    PHPUnit + Bruno (fail-fast)
        |
        |-- success -> promote: down -v + mv projectfiles-test -> projectfiles/
        |-- failure -> deployment blocked, promote not executed
        v
[4] build-{env}-runner
    deploy to stage/prod


Key idea: **temporary directory** `projectfiles-test/. Until tests pass, main projectfiles/ remains untouched. Only after a successful run is promote performed: test containers are shut down, and code moves to projectfiles/.


## 3. What Was Already Done on the Development Side

All created new files in the oninvest-deploy-config repository.

File

Purpose

Dockerfile.test

Test Docker image. Inherits from oninvest-img-php8.2, includes PHPUnit

docker-compose-test.yml

Brings up oninvest-backend-test on port **8081** and postgres-test on port **5436**

conf/.env.test

Test env: APP_ENV=test, DB_DATABASE=oninvest_test

build-test-runner.sh

Builds and starts test environment: checks base image, build, kill/rm old containers, up, migrations, fixtures

scripts/run-tests.sh

Runs PHPUnit and Bruno. No build or migrate — only test execution


So I prepared everything so that DevOps only had to integrate it correctly into the Jenkins job.

4. What Needed to Be Done on the Jenkins Side

4.1. Jenkins Plugins

In Manage Jenkins → Plugins, make sure the following plugin is installed:

Plugin

Minimum version

Why

JUnit

1.60+

Parses test-reports/*.xml, builds trend graph


Without it, reports won't show on the build page.

4.2. Bruno CLI on the Target Server

Bruno (bru) runs on the host, not in a container. It needs to send HTTP requests to http://localhost:8081`, where the test backend is running.

Install:

```bash
npm install -g @usebruno/cli
bru --version
```

4.3. Four Shell Steps in Jenkins

In the Build section there should be 4 steps. The logic is:

Step

What it does

Condition

1

Rsync code

If `RUN_TESTS=true — to projectfiles-test/, otherwise directly to projectfiles/

2

Build test environment

build-test-runner.sh: build + up + migrations

3

Tests + promote

run-tests.sh: PHPUnit + Bruno, then down -v and mv projectfiles-test → projectfiles

4

Deploy

As usual, via build-{env}-runne


If RUN_TESTS=false, steps 2 and 3 are skipped, code is rsynced directly to projectfiles/ and deployed.

4.4. RUN_TESTS Parameter

In General → This project is parameterized, add a Boolean Parameter:

- Name: RUN_TESTS
- Default Value: ✅ checked
- Description: Enable PHPUnit + Bruno run before deployment

By default, tests are enabled. If you urgently need to ship a hotfix without tests, you can uncheck it. But that's a deliberate exception, not the normal mode.

4.5. Publishing Reports

In Post-build Actions, add:

- Publish JUnit test result report
- Test report XMLs: test-reports/*.xml

Reports are saved always, even if tests fail. Jenkins keeps the last 30 builds and builds the Test Result Trend graph.

5. DevOps Checklist

- [ ] Install Bruno CLI: npm install -g @usebruno/cli, check bru --version
- [ ] Verify Jenkins JUnit plugin 1.60+
- [ ] Update the job: 4 Shell steps
- [ ] Configure RUN_TESTS parameter (Boolean Parameter, default checked)
- [ ] Add Post-build Action: Publish JUnit test result report with path test-reports/*.xml
- [ ] Run the first build and make sure all steps pass
- [ ] Check that the **Test Result Trend** graph appears on the build page

6. The Result

The main result: deployment became safer. Now you can't accidentally ship code with failing tests: the pipeline stops, promote isn't executed, and the main directory remains untouched.

What else matters:

- Isolation. The test environment lives separately: its own containers, its own DB, its own ports.
- Fail-fast. PHPUnit or Bruno fails — deployment is blocked.
- Observability. JUnit reports and trend graph in Jenkins.
- Flexibility. RUN_TESTS lets you skip tests, but they are enabled by default.
- Separation of responsibilities. The developer prepared the test harness, DevOps integrated it into Jenkins.

On dev environments, the scheme already works. The next step is to carefully enable it in production, add more tests, and monitor execution time so feedback remains fast.

Share: