Testing Using Jest
Tags: opensource
Published: 2021-11-12 22:20:44

## Intro

This week, I continued working on my static site generator [Jellybean](https://github.com/lyu4321/jellybean) and focused on creating and running tests for the program. I decided to use [Jest](https://jestjs.io/) for testing because it was what was recommended and also because I have tried other tools such as Jasmine and Karma before, but not Jest, so I thought this would be a good opportunity to learn how it works. 

## Testing Process

By following the documentation, I installed Jest by running:

```bash
npm install --save-dev jest
```

and adding:

```json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch --",
    "coverage": "jest --collectCoverage --",
  }
}
```

to the scripts in my package.json file. This allows the user to choose from three different commands to run tests:

`npm run test` specifies that the tests should be run. 
`npm run test:watch` specifies that the tests runner should watch for changes and if a change is made, the tests will be run automatically.
`npm run coverage` specifies that the tests should be run and a coverage report should be automatically generated.

During this process, I created two types of tests. I created unit tests for a specific function in my program and E2E integration tests to test the entire program. 

The function I decided to test with unit tests is called getHtmlNav, which accepts a string or array as an argument and returns the navbar of a page as a string. 

For example:

```js
test('Creates navbar with link to index page', () => {
    expect(getHtmlNav()).toBe(
        `<div><ul><li><a href='./index.html'>Home</a></li></ul></div>`
    );
});
```

When no arguments are passed, the function should return a navbar with a single link to the index page. 

```js
test('Creates navbar with link to index and user-defined pages (array of files)', () => {
    expect(
        getHtmlNav([
            'Silver Blaze.txt',
            'The Adventure of the Six Napoleans.txt',
        ])
    ).toBe(
        `<div><ul><li><a href='./index.html'>Home</a></li><li><a href='./Silver Blaze.html'>Silver Blaze</a></li><li><a href='./The Adventure of the Six Napoleans.html'>The Adventure of the Six Napoleans</a></li></ul></div>`
    );
});
```

When an array of strings is passed, the function should return a navbar with links to the index page and pages that are passed in the array (but the extension should be changed from .txt or .md to .html). There are several other scenarios, such as when a string is passed or when an empty array is passed, which I also tested for. 

In addition, I created E2E tests for the entire program.

I created a file called run.js which runs the program using execa and another file that contains the tests. An example of an E2E test would be:

```js
    test('Print error message when no input', async () => {
        const { stderr, stdout, exitCode } = await run();
        expect(exitCode).toBe(1);
        expect(stderr).toMatchSnapshot();
        expect(stdout).toEqual('');
    });
```

Which runs the program with no command line arguments. This expects the exitCode to be 1 with a stderr because arguments are required. 

One thing that my program didn't test for previously was a input folder that was empty. If a folder does not contain any .txt or .md files, then the program should not continue and should instead return an error message prompting for valid input. 

```js
    test('Prints error message when empty folder', async () => {
        const { stderr, stdout, exitCode } = await run(
            '--input',
            'invalid-folder'
        );
        expect(exitCode).toBe(1);
        expect(stderr).toMatchSnapshot();
        expect(stdout).toEqual('');
    });
```

This was the test that I wrote for the scenario and I had to go back and change my code so that if an input folder was empty or did not contain any valid files, the program would return an error:

```js
let files = fs.readdirSync(input);
                    let filesArray = files.filter(
                        (f) =>
                            path.extname(f) == '.txt' ||
                            path.extname(f) == '.md'
                    );
                    if (filesArray.length == 0) {
                        console.error('Input directory is empty.');
                        process.exit(1);
```

I created a testing branch to add all of these changes and I used an [interactive rebase](https://github.com/lyu4321/jellybean/commit/b9b9d8199a81d4872f202f5042e17b8baf4a5980) to squash all the changes together and merged them with the main branch. 

## Conclusion

Although testing is an essential part of software development, I don't have much experience with writing or running tests so this was a great opportunity to learn about two types of tests, unit and E2E integration tests. The tests I have written only cover a small portion of what is required for the entire program so I will definitely need to make adjustments in the future. However, I will definitely continue using the the tools and skills I learned during this process in my future projects.