Skip to main content
GitHub Actions
CHAPTER 07

Automated Testing with GitHub Actions

Updated: May 15, 2026
25 min read

# CHAPTER 7

Automated Testing with GitHub Actions

1. Introduction

If Continuous Integration (CI) is the heart of DevOps, Automated Testing is the brain. A pipeline that only installs dependencies and checks syntax is just a glorified download script. The true purpose of GitHub Actions is to act as a merciless quality gatekeeper. It must run a suite of automated tests against the code, and if even a single test fails, GitHub must reject the Pull Request. In this chapter, we will integrate automated testing frameworks into our workflow, understand exit codes, and utilize services like MySQL to run database tests.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Understand the role of Unit Testing within a CI pipeline.
  • Execute testing frameworks (like PHPUnit or Jest) inside GitHub Actions.
  • Understand how GitHub Actions interprets terminal exit codes to fail jobs.
  • Configure "Services" to spin up temporary databases for testing.
  • Block Pull Requests based on test status checks.

3. Beginner-Friendly Explanation

Imagine an automated spell-checker before publishing a book.
  • The Developer: Writes a new chapter of a book and clicks "Submit Pull Request".
  • The Code: Contains a massive typo: "The quick brown fxo."
  • GitHub (Without Testing): Automatically merges the book, prints 10,000 copies with the typo, and ships them to stores.
  • GitHub (With Testing): Runs an automated spell-check script. The script finds "fxo", throws a red error, and halts the pipeline. GitHub blocks the merge button and emails the author: "Fix your typo." The bad book never reaches the customer.

4. How GitHub Interprets Tests (Exit Codes)

GitHub Actions doesn't know how to test PHP or JavaScript inherently. It simply runs a terminal command (like vendor/bin/phpunit) and watches the Exit Code.
  • If the test suite finishes and returns an exit code of 0 -> GitHub turns the job Green (Success).
  • If the test suite returns an exit code of 1 (or any non-zero number) -> GitHub immediately turns the job Red (Failure) and stops executing any further steps.

5. Using Services (Databases for Testing)

Many applications cannot be tested without a database. You cannot run mysql directly on the standard Ubuntu runner without complex installation steps. Instead, GitHub Actions provides the services keyword. This spins up a temporary Docker container (like MySQL or Redis) specifically for your job to talk to while testing!

6. Mini Project: Run Automated PHP Tests

Let's add a robust testing stage to the pipeline we built in Chapter 6, including a temporary MySQL database.

Step-by-Step Pipeline Concept:

yaml
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
name: PHP Testing Pipeline
on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    # Spin up a temporary MySQL database for the tests!
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'

      - name: Install Dependencies
        run: composer install --prefer-dist

      - name: Setup Environment File
        run: cp .env.example .env

      - name: Run Database Migrations
        # The app connects to the 'services' database defined above!
        run: php artisan migrate 
        env:
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: test_db
          DB_USERNAME: root
          DB_PASSWORD: root

      - name: Execute Tests (PHPUnit)
        # If this fails, GitHub turns the step Red and stops.
        run: vendor/bin/phpunit

7. Real-World Scenarios

An e-commerce company updated their website's shopping cart code. The developer tested it manually and it "looked fine." They merged the Pull Request. A minor logic error in the code caused the "Calculate Tax" function to always return $0.00. The bug was in production for three days before accounting noticed. The company lost thousands of dollars in uncollected taxes. If they had an automated GitHub Actions pipeline that strictly required passing Unit Tests for the tax calculation logic, the phpunit command would have returned an exit code of 1, GitHub would have blocked the merge, and the bug would never have reached production.

8. Best Practices

  • Require Status Checks: In your GitHub Repository Settings -> Branches -> Branch Protection Rules, you must check "Require status checks to pass before merging" and select your test job. If you don't do this, a developer can simply ignore the red X and click "Merge" anyway! Automation only works if it is enforced.

9. Security Recommendations

  • Test Database Passwords: In the services block above, we used MYSQLROOTPASSWORD: root. Because this is a temporary, disposable database that only exists for 3 minutes during the test and is not accessible to the public internet, using a weak password here is acceptable and standard practice. *Never* put your production database password in this file!

10. Troubleshooting Tips

  • Database Connection Refused: If your tests fail saying they cannot connect to MySQL, it is usually because the MySQL container takes 10-15 seconds to boot up, and your tests tried to run at second 5. The --health-cmd options we included in the services block force GitHub to wait until the database is fully ready before moving to the steps.

11. Exercises

  1. 1. How does GitHub Actions know whether a phpunit or npm test command succeeded or failed?
  1. 2. What is the purpose of the services block in a workflow YAML file?

12. FAQs

Q: Can I generate a visual test report instead of just reading terminal logs? A: Yes. Many testing frameworks can output a "JUnit XML" file. You can use marketplace actions like EnricoMi/publish-unit-test-result-action to parse this XML and post a beautiful summary table directly as a comment on the Pull Request.

13. Interview Questions

  • Q: Explain the concept of ephemeral test databases in GitHub Actions. How does the services block facilitate integration testing without relying on external, long-lived staging databases?
  • Q: A developer complains that their Pull Request was blocked by GitHub Actions, but when they run the tests on their local laptop, everything passes. As a DevOps engineer, what discrepancies between the local environment and the runner environment would you investigate first?

14. Summary

In Chapter 7, we transformed GitHub Actions from a simple script runner into a strict quality gatekeeper. We learned that CI pipelines rely entirely on terminal exit codes to determine the health of the software. We successfully integrated an automated testing framework and leveraged the powerful services block to conjure temporary, isolated databases, ensuring our integration tests run in a pristine environment every time. By coupling these workflows with GitHub Branch Protection rules, we guarantee that no buggy code can ever be merged into our production branch.

15. Next Chapter Recommendation

Our code is verified, tested, and approved. It is time to deliver it to the world. Proceed to Chapter 8: Deploying Applications with GitHub Actions.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·