Running MySQL Tests With GitHub Actions
Recently, I have been talking about how to write and run tests for Node.js code that interacts with a MySQL database. These posts have included details on how to use third-party libraries such as Testcontainers to run the tests in a clean database and Knex to manage database changes. Today, I will discuss how to automate running these tests using GitHub Actions when code is pushed to a repository.
We can write comprehensive tests that verify that code is doing what we expect to do, but if those tests are not run on a consistent basis, they lose their value. While developers should be writing, updating, and running tests whenever they update code - and especially before those code changes are pushed out to a code repository - the truth is, we don’t always follow those rules. Sometimes, we need to implement a quick fix because of a critical bug, and testing may go to the wayside while trying to fix the issue. Sometimes we are just lazy. Sometimes, we may forget. This can cause unnecessary downtime due to untested code making it to production and wreaking havoc.
Running tests should be a part of any continuous integration (CI) or continuous deployment (CD) workflow. We can manage this by using GitHub Actions. GitHub Actions allows us to automate our workflows to build, test, and deploy our code whenever there are changes to a repository - a push to a repo, a pull request being created, issues being filed, etc.
In this post, we will focus on the
push event. In other words, we will configure GitHub Actions to run our tests whenever code is pushed to a particular branch.
Getting the Code
To get the code in this demo, head over to this GitHub repo and clone it. This code is from the demo we discussed in this post. I moved it to a separate repo to prevent these tests from running when I am working on different demos.
The command to clone the repo from the command line over SSH is:
git clone email@example.com:boyzoid/github_actions_testcontainers.git
Next, change the directory for this demo.
Lastly, if you want to run the tests locally, run the following command:
The directory structure should resemble the image below when done with these steps.
I will talk about what is different in this code. Specifically, the
When using GitHub Actions, we define a workflow by creating a folder with the path
.github/workflows/file_name.yml. In our case, the file is named
node.test.yml. You can have multiple workflows in a given project that each perform different actions when different events are fired.
Here is what our workflow looks like. We will break this down section by section.
name: Node.js Tests on: push: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [20.x] steps: - uses: actions/checkout@v3 - name: Use Node.js $ uses: actions/setup-node@v3 with: node-version: $ cache: 'npm' - run: npm ci - run: npm test
name property is pretty easy to figure out. It is the name of our workflow.
on property defines what action will prompt the workflow to be executed. Here we are using
push and the sub-property
["main"] to indicate we want this workflow to be executed when a
push is made to the branch named
main. Note the
branches value is an array, so we could add other branches to this value.
Next, we use the
jobs property to define what gets executed when the workflow is processed. Here we have a single
runs-on property defines the type of machine to run the job on. Here, we are using a GitHub-hosted runner with the latest version of Ubuntu.
strategy.matrix property, we can set variables in a single job that will create multiple job runs based on the combination of those variables. Using a matrix, we can test our code on different language or operating system versions. In our case, we are running our tests using version
20.x of Node.js.
Next, we have the
steps property, an array of processes we want to perform in our job. Some of these steps use pre-defined actions that are available through GitHub Actions.
- The first step uses the pre-defined action named
checkout. This step does what you think it does. It checks out the code from GitHub to the runner. In this case, it checks out the
- The next step is named
Use Node.js $and also uses a pre-defined action. This action is named
actions/setup-node@v3. This action uses the values from
strategy.matrixto set up the specified version(s) of Node.
- The third step runs the command
npm ci, which will install all the dependencies defined in our
- The fourth step runs the command
npm test, the command we use to run our tests.
Enabling the Workflow
The way we enable each workflow we define is pretty simple. We commit the YAML file(s) and then push these changes to the remote repo on GitHub. GitHub takes over from there.
Running the Workflow
Our workflow will be run anytime code is checked into the
main branch in our repository. You can view the progress of a running workflow or the history of workflows on the ‘Actions’ tab in GitHub.
Actions page, we will see a list of all the workflows that have been run. We will see the commit message used when the code was pushed (1), the workflow name (2), the commit ID (3), and the person who committed the code (4).
We can see details about the run if we click the commit message. We can see the status (1), how long the run took (2), and details about the workflow itself (3).
If we click the
1 Job completed link, we will see a list of jobs that were completed. In our case, we will see one named ‘build 20.x’. When we click the
build 20.x link, we can see fine details about the workflow.
This view lists everything done when running the workflow, including setting up and breaking down the runner. Since we are concerned about the tests running successfully, we should focus on the details under
Run npm test.
> firstname.lastname@example.org test > node --test TAP version 13 # Subtest: Testing Application # Subtest: Container should be running ok 1 - Container should be running --- duration_ms: 89265.867539 ... # Subtest: Testing Migration # Subtest: User table exists ok 1 - User table exists --- duration_ms: 7.070651 ... # Subtest: User Type table exists ok 2 - User Type table exists --- duration_ms: 4.415232 ... 1..2 ok 2 - Testing Migration --- duration_ms: 12.363089 ... # Subtest: Testing Seed # Subtest: User data exists ok 1 - User data exists --- duration_ms: 15.890114 ... # Subtest: User Type data exists ok 2 - User Type data exists --- duration_ms: 25.731585 ... 1..2 ok 3 - Testing Seed --- duration_ms: 42.748207 ... # Subtest: Testing User Repo # Subtest: Can add user ok 1 - Can add user --- duration_ms: 23.006665 ... 1..1 ok 4 - Testing User Repo --- duration_ms: 24.088673 ... 1..4 ok 1 - Testing Application --- duration_ms: 89618.999211 ... 1..1 # tests 10 # suites 0 # pass 10 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 90219.293716
The information above shows the individual output from running each of our tests and a summary at the end. We can see that 10 tests were run, and 10 passed. If any tests fail, we will see indications of that in the
Actions tab and an email will be sent.
Here is what the details look like in the GitHub web interface.
When we click through and look at the details of the job, we will see the following:
> email@example.com test > node --test TAP version 13 # Subtest: Testing Application # Subtest: Container should be running ok 1 - Container should be running --- duration_ms: 24339.372971 ... # Subtest: Testing Migration # Subtest: User table exists ok 1 - User table exists --- duration_ms: 4.348173 ... # Subtest: User Type table exists ok 2 - User Type table exists --- duration_ms: 2.615384 ... 1..2 ok 2 - Testing Migration --- duration_ms: 7.682252 ... # Subtest: Testing Seed # Subtest: User data exists ok 1 - User data exists --- duration_ms: 5.204167 ... # Subtest: User Type data exists ok 2 - User Type data exists --- duration_ms: 8.624846 ... 1..2 ok 3 - Testing Seed --- duration_ms: 14.590808 ... # Subtest: Testing User Repo # Subtest: Can add user not ok 1 - Can add user --- duration_ms: 12.525421 location: 'file:///home/runner/work/github_actions_testcontainers/github_actions_testcontainers/test/knex-demo.test.js:63:17' failureType: 'testCodeFailure' error: |- Expected values to be strictly equal: 3 !== 4 code: 'ERR_ASSERTION' name: 'AssertionError' expected: 4 actual: 3 operator: 'strictEqual' stack: |- TestContext.<anonymous> (file:///home/runner/work/github_actions_testcontainers/github_actions_testcontainers/test/knex-demo.test.js:74:20) process.processTicksAndRejections (node:internal/process/task_queues:95:5) async Test.run (node:internal/test_runner/test:632:9) async TestContext.<anonymous> (file:///home/runner/work/github_actions_testcontainers/github_actions_testcontainers/test/knex-demo.test.js:63:9) async Test.run (node:internal/test_runner/test:632:9) async TestContext.<anonymous> (file:///home/runner/work/github_actions_testcontainers/github_actions_testcontainers/test/knex-demo.test.js:58:5) async Test.run (node:internal/test_runner/test:632:9) async startSubtest (node:internal/test_runner/harness:208:3) ... 1..1 not ok 4 - Testing User Repo --- duration_ms: 13.361816 location: 'file:///home/runner/work/github_actions_testcontainers/github_actions_testcontainers/test/knex-demo.test.js:58:13' failureType: 'subtestsFailed' error: '1 subtest failed' code: 'ERR_TEST_FAILURE' stack: |- async TestContext.<anonymous> (file:///home/runner/work/github_actions_testcontainers/github_actions_testcontainers/test/knex-demo.test.js:58:5) ... 1..4 not ok 1 - Testing Application --- duration_ms: 24628.972343 location: 'file:///home/runner/work/github_actions_testcontainers/github_actions_testcontainers/test/knex-demo.test.js:8:1' failureType: 'subtestsFailed' error: '1 subtest failed' code: 'ERR_TEST_FAILURE' ... 1..1 # tests 10 # suites 0 # pass 7 # fail 3 # cancelled 0 # skipped 0 # todo 0 # duration_ms 25010.234841 Error: Process completed with exit code 1.
Note that it shows 10 tests were run, 7 passed, and 3 failed.
If you have been following along in this series, remember that our tests used Testcontainers as the source for our MySQL database. You might have noticed that we did not accommodate this in our code or YAML file. This is because, by default, the GitHub-hosted runners already have Docker running, so we do need to take any special steps to get our tests to run correctly.
CI/CD workflows allow for automated code builds and deployment. Part of those processes should be running tests to verify the code is functioning as expected. With GitHub actions, we can set up workflows to not only build and deploy but to run tests as well. Using GitHub Actions allows us to run our tests using the same process and even the same command to run our tests every time code is pushed to our repo.