Intro
After writing a few unit and E2E Integration tests for my static site generator, Jellybean, last week, this week I added Continuous Integration to the repository. It was very easy to set up and is useful if you or other contributors forget to manually test while making changes. It ensures that changes or pull requests made to the main branch are automatically tested by running a workflow.
Setting up GitHub Actions CI Workflow
The process of setting up the workflow was very simple. In the "Actions" tab of my repository, I selected a workflow template for my program. In my case, I selected the Node.js template, which automatically created a .github/workflows folder in my repository containing a node.js.yml file:
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test
The only thing I changed in the default template was the supported Node versions. By default, versions 12.x, 14.x, and 16.x are supported. However, I was using a function in my program called fs.rmSync(), which is not supported in version 12.x and I was running my program on version 14.17.5. Therefore, I decided to remove the option for version 12.x from the template and put a note about which version of Node to install in the README and CONTRIBUTING.md files.
For my own project, I wrote a few more unit tests for two functions, just to ensure that the workflow was properly set up and working and my changes passed all the checks. I merged these changes to the repository.
I also worked on another project, mh-ssg, and added tests to this project as well. I created unit tests for a function called processFolder(). This function accepts an input folder path, output folder path and stylesheet URL. I created two unit tests, one which tests when a valid input folder containing files is passed to the function and one which tests when a non-existent folder is passed to the function. The function logs a message to the console depending on how many files are saved in the output folder, so the tests I wrote tested for these console logs.
const MOCK_FILE_INFO = {
"/path/text.txt": "Hello World!",
"/path/markdown.md": "This is a markdown file",
"/output": []
};
beforeEach(() => {
require("fs").__setMockFiles(MOCK_FILE_INFO);
});
This part of the code sets up a mock file system. This is different from my own tests, as I used a real file system, but I think that in the future, I will change to using a mock file system as well because it reduces the need to create extra files.
test("should log success message to console", () => {
const mockSpy = jest.spyOn(console, 'log');
processFolder("/path", "/output", "https://cdnjs.cloudflare.com/ajax/libs/tufte-css/1.8.0/tufte.min.css");
expect(mockSpy).toHaveBeenCalledWith(`${chalk.green(
`2 file(s) saved to folder /output successfully!`
)}`);
});
test("should log success message to console", () => {
const mockSpy = jest.spyOn(console, 'log');
processFolder("/nonexistent", "/output", "https://cdnjs.cloudflare.com/ajax/libs/tufte-css/1.8.0/tufte.min.css");
expect(mockSpy).toHaveBeenCalledWith(`${chalk.green(
`0 file(s) saved to folder /output successfully!`
)}`);
});
This part of the code contains the tests. I had to do some research on how to capture console logs in my tests and I discovered the jest.spyOn() function. By assigning mockSpy to jest.spyOn(console, "log"), I spied on the console.log function while running the function I was testing, and I could check to see whether it was called and the arguments it was called with. When passing a mock directory with two valid files, I expected console.log to have been called with:
`${chalk.green(
`2 file(s) saved to folder /output successfully!`
)}`
(Chalk is used to style the console output)
However, when passing a directory that doesn't exist, I expected console.log to have been called with:
`${chalk.green(
`0 file(s) saved to folder /output successfully!`
)}`
My tests passed and I created a merge request to the repository. The repository owner, Minh Hang, approved running the workflow on my pull request, which passed, and merged my changes.
Minh Hang also created a PR for adding tests to my repository. When creating a PR in a repository for the first time, usually the owner needs to manually approve running the workflow and Minh Hang's changes looked good to me so I did so and all the checks passed. I was able to merge her changes into my repository as well.
Conclusion
If you work on any repository with others, you will probably come across CI sooner or later. Almost every repository I've contributed to uses them and sometimes my changes would work locally but fail when I created a merge or pull request and it would be hard to figure out why as I had never used CI in my own projects before. After learning more about testing and CI, I understand the benefit and importance of using them in projects and will continue to use them in the future.
Top comments (1)
What tools do you use for CI/CD : Jenkins, CircleCI ?