# Running unit tests

This section will tackle our very first task: running unit tests.

## Constructing workflows

The most fundamental questions that Github Actions workflows answers are:

1. When will it run?
2. What will it do?

"When will it run?" is answered by specifying the events that the workflow responds to. In our scenario, we want this workflow to run when a pull request is created on the repository.

"What will it do?" can be then broken down into several more guiding questions?

1. Which OS should this run in?
   1. Are there key differences between OSes that should be accounted for?
2. What programming language is this project written in?
3. What steps should be run to achieve the given workflow?
4. Can certain tasks be split into separate jobs and run in parallel?
5. What are jobs that depend on others?

We will take a look at how we can answer questions (4) and (5) in a following section. Let's first tackle questions (1), (2), and (3) in our scenario.

1. In the case of `calculator.test.ts`, the OS we run it on does/should not matter as there are no OS specific test cases. For simplicity, we will pick Ubuntu.
2. The project is written in React, so it depends on having Node.js available.
3. We first need to get the repository, install all necessary dependencies, and run the `test` script that is provided (see `package.json`)

{% hint style="success" %}
You might be asking yourself, "What if I have unit tests that are specific to an OS? Or version of Node.js? Or Ubuntu version?"\
\
Github Actions supports something it calls [matrix strategies](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow), that run a given workflow across a matrix of variables. We will briefly dive into it in the [cookbook](https://wiki.nushackers.org/hackerschool/ci-cd-with-github-actions/cookbook "mention")!
{% endhint %}

## Designing the workflow

Now that we have clearly outlined the key details of this workflow, let's get down to writing your very first workflow file. Remember, all Github Actions workflows must reside in `.github/workflows`, so create a new file called `ci.yml`!

{% hint style="info" %}
Github Actions uses a [YAML](https://www.redhat.com/en/topics/automation/what-is-yaml)-based specification language to define its workflows. It might seem a little strange at first, but it becomes more intuitive as you work with it.\
\
For a crash course on YAML syntax, refer to [this guide.](https://www.cloudbees.com/blog/yaml-tutorial-everything-you-need-get-started)
{% endhint %}

<pre class="language-yaml" data-full-width="false"><code class="lang-yaml"><strong># .github/workflows/ci.yml
</strong><strong>name: CI/CD Pipeline
</strong>on: [pull_request, workflow_dispatch]
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch repository
        uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'
      - name: Install dependencies
        run: |
          yarn
      - name: Run unit tests
        run: |
          NODE_ENV=production yarn test
</code></pre>

## Breaking it down

Let's break down each section and explain what we're doing.

### Workflow name

```yaml
name: CI/CD Pipeline
```

We are giving the workflow a name that Github Actions will display. If the name is omitted, GitHub displays the workflow file path relative to the root of the repository.

### Workflow triggers

```yaml
on: [pull_request, workflow_dispatch]
```

Then, we specify the times where this workflow runs, aka "when will it run?". They are specified with the `on` key and the values correspond to the [list of events that trigger a workflow](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).&#x20;

{% hint style="info" %}
In this case, we specify the list of events as an array in YAML. However, there are instances where you might want to specify additional conditions on when the workflow may run (such as only executing when the pull request's target branch is the `main` branch). In that case, you will need to use the YAML dictionary definition. We will cover this in a bit.
{% endhint %}

In our case, we know that we want to run the workflow during a pull request, so we have included the `pull_request`  event. We have also included the `workflow_dispatch` event, so that we are able to manually trigger this workflow from Github without requiring a pull request (more about it [here](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch)). This is particularly useful if we want to verify that the workflow works without going through the hassle of creating a pull request.

### Defining jobs

```yaml
jobs:
  unit-tests:
```

We then start to specify the jobs that comprise the workflow. We give the job a job ID of `unit-tests`.

```yaml
    runs-on: ubuntu-latest
```

Recall that we said that every job runs in its own runner, which is a virtual machine by default. Therefore, we need to specify the OS that our `unit-tests` job will execute in, which we have decided earlier to be Ubuntu. As we are not particular about the version of Ubuntu we will use, we can use the `ubuntu-latest` , which is one of the [many available Github-hosted runners](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners).&#x20;

This means that we can think of every step of this job executing within an Ubuntu virtual machine (because they really do!). So we will be using `bash` commands, and we will have access to things like the [`apt` package manager](https://documentation.ubuntu.com/server/how-to/software/package-management/index.html) that is available in Ubuntu.

### Fetching the current repository

```yaml
    steps:
      - name: Fetch repository
        uses: actions/checkout@v4
```

We declare the steps of a job under the `steps` array as a list of dictionaries. As we are running the job in a virtual machine, we will basically have an empty machine at the start of the job, and it is our responsibility to start populating and interacting with this empty machine to create the intended workflow.&#x20;

The very first step we need to do is fetch the current repository, so that the virtual machine runner has access to the project structure, and more importantly, the unit tests. We give the step a human readable name (that is also displayed on Github) using the `name` key. If there is no name provided, Github will display the script or action that is being used.&#x20;

Here, we use an action — which are reusable extensions that perform some set of operations — called `actions/checkout@v4`. You can read more about what the action is comprised of [here](https://github.com/actions/checkout), but essentially, we are using it to perform a checkout of the current repository, retrieving all of its contents (from the latest commit) onto the virtual machine runner.

{% hint style="info" %}
The virtual machine runner has a root directory defined as `$GITHUB_WORKSPACE` . Without any other inputs for the `actions/checkout@v4` action, we default to fetching the contents of the repository directly into the `$GITHUB_WORKSPACE` directory.\
\
If you are wondering how you can verify this information, you can refer to the appendix's [debugging Github Actions](https://wiki.nushackers.org/hackerschool/cookbook#debugging-github-actions) which covers how to do so!
{% endhint %}

### Setting up Node.js

```yaml
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'
```

Then, we need to setup Node.js on the virtual machine runner. We can use the action `actions/setup-node@v4` to setup Node.js automatically for us. You can read more about the action [here](https://github.com/actions/setup-node).

We can specify inputs for the action using the `with` key, providing the various inputs as a dictionary. In this case, we want to use Node.js version 20, and we want to use the `yarn` package manager — instead of `npm` — as that is what we have used for the example application.

### Installing dependencies

```yaml
      - name: Install dependencies
        run: |
          yarn
```

Before we can run the unit tests, we need to ensure that all of the necessary project dependencies are retrieved. This is where we can use scripts in steps.

Recall in [#defining-jobs](#defining-jobs "mention"), we mention that all steps in the job are effectively running on Ubuntu, so we will specify scripts that use Ubuntu's built-in shell: `bash`. You might want to specify a different shell for certain use cases, which [you can do so here](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell).

So, we declare our script through the `run` string. The pipe operator after the `run` key indicates that we are specifying a multi-line string in YAML.

The script we will run this time is `yarn`, which effectively installs the project's dependencies. You can think of this as running `yarn` directly in an Ubuntu terminal, where the current folder (`$GITHUB_WORKSPACE`) is the root of the Node.js project.

{% hint style="info" %}
Again, if you are wondering how you can verify this information, you can refer to the cookbook's recipe on [debugging Github Actions](https://wiki.nushackers.org/hackerschool/cookbook#debugging-github-actions) which covers how to do so!
{% endhint %}

### Running unit tests

```yaml
      - name: Run unit tests
        run: |
          NODE_ENV=production yarn test
```

Finally, we can start to execute the unit tests of the project. Again, we use a script, but this time, the script will be `NODE_ENV=production yarn test` which effectively sets the environment variable `NODE_ENV` to be of value `production`.

`test` is a script that we have provided in the example application, and it essentially runs `vitest` , executing `calculator.test.ts`.

Voilà! You have successfully written your very first Github Actions workflow! Simple isn't it? Let's recap what we did.

## Visualizing the workflow

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FtjiBMSfSLcqO53ho6wvm%2Fcicd-pipeline-unit-tests.png?alt=media&#x26;token=53287741-5d37-47b9-9b7a-7fd46b6c73f4" alt=""><figcaption></figcaption></figure>

At a glance, this is the high-level overview of the new CI/CD pipeline you have written.

To properly visualize and understand how the filesystem of the virtual machine runner changes throughout the job, we have also created this visualization (bolded text are the changes between steps):

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FkGFd3PpuO7Bak8UVt5bI%2Funit-tests-filesystem.png?alt=media&#x26;token=0b554910-a857-4cd4-81fe-c490cb10b11f" alt=""><figcaption></figcaption></figure>

## Verifying the workflow

### Manually running workflows

Once you have added the workflow, you need to commit and push it!

```
git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push -u origin main
```

Then, we can start to verify that the Github Action works as intended! This is where the `workflow_dispatch` event comes in handy, where we are able to manually trigger Github Actions. If you navigate to your fork of the example repository, you can visit the Actions tab. You will see the following:

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2F42c1BiYJtsggN2Ct0ay6%2Fimage.png?alt=media&#x26;token=b57207c8-98b5-4ba8-913c-557aef866b74" alt=""><figcaption></figcaption></figure>

It lists the workflows available, and what has run/are running. We are interested in our new pipeline `CI/CD Pipeline`, so select it and you should see the following:

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FDkue9LJ5DAHzrdcgeDuX%2Fimage.png?alt=media&#x26;token=e39f70ca-1af8-40d9-adfa-6f20b61b4d5c" alt=""><figcaption></figcaption></figure>

It looks almost the same, but this time, there is a banner that tells you that "This workflow has a `workflow_dispatch` event trigger". Then, there is a dropdown to "Run workflow", select it and stick with `main` and click the "Run workflow" button:

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FBdmpz5gPAZe8SbyFaeUP%2Fimage.png?alt=media&#x26;token=8e2c135a-d818-422d-a97a-9d8a81428dfc" alt=""><figcaption></figcaption></figure>

Refresh the page, and you will now see a new entry in the table:

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FWBRuOCPMGwOoW1MjPrCz%2Fimage.png?alt=media&#x26;token=c9b7b1f3-e95b-48e7-b0e5-66b5d221bf48" alt=""><figcaption></figcaption></figure>

Give it a few seconds and then click into the action. You will see that the unit tests have failed:

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FlzdhUTpMzLuPv0GZbJ9M%2Fimage.png?alt=media&#x26;token=90f6e8c1-3c93-407e-ad47-a6e7c2a04463" alt=""><figcaption></figcaption></figure>

This is because we have intentionally made one of the unit tests to fail (divide ½ is not 0.4!). Let's try to fix this unit test while exploring the the `pull_request` event!

### <kbd>pull\_request</kbd> event

{% hint style="warning" %}
The following steps expects some level of understanding of Git. You can refer to our [Git guide for more information](https://wiki.nushackers.org/orbital/git)!
{% endhint %}

This guide is not a software engineering exercise, so we will tell you exactly where the error is and we will focus on examplifying the `pull_request` event.

We had intentionally set one of the unit test assertions to be incorrect, specifically along [these lines](https://github.com/woojiahao/cicd-calculator/blob/main/src/calculator.test.ts#L12-L14):

```javascript
test('divide 1 / 2 to equal 0.5', () => {
  expect(divide(1, 2)).toBe(0.4);
});
```

We have set the expected value to be `0.4` when it should clearly be `0.5` ! Let's fix this as a pull request to your own repository to see the `pull_request` event in action.

Create a new branch, called `fix-unit-test`:

```
git checkout -b fix-unit-test
```

Then, go to the file `calculator.test.ts` and fix line 13 to be the following:

```javascript
  expect(divide(1, 2)).toBe(0.5);
```

Then, add the file and create a commit. The commit message can be anything you want:

```
git add src/calculator.test.ts && git commit -m "Fix unit test"
```

Finally, push the branch to your fork:

```
git push -u origin fix-unit-test
```

Then, go to your fork on Github and create a new pull request. Pull request > New pull request. Give the pull request any title and you can leave the description blank.&#x20;

{% hint style="warning" %}
The base branch should be YOUR `main` branch, not the original repository's branch!
{% endhint %}

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FIJxiAiIR6gYiwKhXFMG4%2Fimage.png?alt=media&#x26;token=c53ff2aa-0e3a-4bc2-b2ef-b1b77fefd0cc" alt=""><figcaption></figcaption></figure>

Create the pull request and wait a while. You will notice that the component towards the bottom changes to this:

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2FODB5ouQfTpwpXmE40GKy%2Fimage.png?alt=media&#x26;token=b11c5638-d25d-4918-a69a-53b9d03bec4e" alt=""><figcaption></figcaption></figure>

This is how you know that your workflow is running and it was triggered by the `pull_request`!

Now, if you select the CI/CD pipeline running, you will be brought back to the same page as earlier, instead, this time, you will notice that the workflow passes!

<figure><img src="https://2807223923-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTUqAJOgHs57S8lmqdxRV%2Fuploads%2Fr4RHfzIY8v4TNTUAGaDt%2Fimage.png?alt=media&#x26;token=fcda5561-cd34-4659-ad8e-1badfed83320" alt=""><figcaption></figcaption></figure>

In fact, you will even see the individual steps of the job `unit-tests` that you defined! Wonderful! You have successfully:

1. Created a new Github Actions workflow
2. Observed how a failing unit test might look like
3. Fixed and verified that the `pull_request` event is working

Go ahead and merge the pull request into `main` and update your local repository to receive the latest changes:

```
git checkout main
git fetch origin main && git merge origin/main
```

Now that we have implemented the very first step, let's take a look at implementing step 2: linting!
