We have set up a CI/CD pipeline that will automatically run tests and deploy the code to heroku when new pull requests are made. There are a lot of moving parts, so let's go through it step by step.
To start developing, make sure you have created the .env
files and did the setup step as described in the main README. This will run both the client and the server. If you want to run them in separate terminals you can run npm run dev
in each folder to just start that one.
The client will be built using webpack
. The configuration for that is in the webpack.config.js
file. When running in production the command npm run build
will be run which will create a dist
folder with the compiled version of the code. When running in dev mode the webpack-dev-server
will build it in memory. This will also automatically rebuild on file changes!
Our client will always run on https://localhost:8080
. So if it does not automatically open your browser you can go to that URL. It will connect to the URL put in the .env
file, defaulting to https://localhost:5000
.
You will also notice that to connect to the server our useFetch
hook adds /api
to the url. This is because on heroku
our backend not only has its own routes but also hosts the client code. This way it allows us to differentiate between what needs to return the client code and what is an actual request to our backend.
The server will be run from the index.js
file. This is separate from the app.js
for testing purposes. In dev mode it will be ran using nodemon
, in production using node
. The .env
determines the port and the URL to the mongodb database. If you change the port here, then don't forget to also change it on the client side.
You probably want to set up your own personal mongodb database for your own usage so that you don't interfere with other developers and use that url most of the time.
There are two lines of MONGODB_URL
in the .env
file. This is because one should be used for cypress
and the other for regular development. We've added an extra failsafe in the seeding code to only do that when connected to the cypressDatabase
so that we don't accidentally erase someone's data.
On both the client and the server side we use the jest
framework for testing. As mentioned in the README each side has their own extra libraries to mock out certain parts to create isolated tests. The jest configuration of each side is in the jest.config.js
file.
In both the client and the server you can run the command npm run test:watch
to have the tests automatically run whenever you change something. This will speed up your ability to fix tests.
If you want to check the code coverage of the tests, run the command npm run test:coverage
! This can help you identify if you have missed some parts. 100% coverage is generally not possible, nor does 100% means that it is tested perfectly, but it is a tool you can use.
On the client side, we want to unit test our components by isolating them. You can use jest-fetch-mock
to mock any fetches made to the api. We use the __testUtils__
folder to combine these responses in one place so that all of our tests can use them. Our api is subject to change, so it will also help make these changes less impactful.
Any utility functions should have their own test, there is a __test__
folder in the util
folder for exactly that purpose.
Remember to clean up the mocks before you start each test that is going to use those mocks by writing the code below at the top of your file:
beforeEach(() => {
fetch.resetMocks();
});
For the client code we want to use the data-testid
attributes as ways to target the elements in our tests. This will serve as points for our QA engineers to interact with the elements as well in the cypress
tests. This is also the reason why every component has a .testid.js
file to store the test ids in. We can then access those in cypress
without having to load all of the client code.
On the server side, we want to test our end points, not our specific controllers/routes. For that we can use supertest
to send requests and then use the mocked database provided by mongodb-memory-server
to make assertions on the database. To create a certain state of the database the __testUtils__
folder provides functions to add things to the database.
Remember that you always want to start every test with a clean database so that other test will not interfere. This is why we have the following code in every test file:
beforeAll(async () => {
await connectToMockDB();
});
afterEach(async () => {
await clearMockDatabase();
});
afterAll(async () => {
await closeMockDatabase();
});
To do our integration test we use cypress
. This will run a browser and allow you to assert using the DOM. Cypress
is a little more difficult to run as it requires all communication with the client and server to go via the browser. It should be totally separate as it needs to test an application as close to reality as possible.
When you want to add or run a cypress
test, you have to first make sure that you are connecting to the cypressDatabase
with your server by adjusting the .env
file. You can open the test GUI by running npm run cypress
from the main directory. From there you can run the tests individually with a view of the browser.
In our CI/CD the tests will be run headless, the command to try that locally is npm run test:cypress
.
The tests should be in the integration
folder. In that folder we try to mimic a bit how the pages are structured in the app, but some deviation is expected.
To access elements in the page, make sure to use the data-testid
or data-elementid
values. There are custom commands to get those in the support
folder. Feel free to add to those!
If your test interacts with the database, remember to add the following code to your test:
beforeEach(() => {
cy.task("db:seed");
});
This will run the /api/test/seed
function which will seed our database with our standard data. If you need more data initially then feel free to add to this function. You will also need to access this data so your test will not fail if the data is slightly changed. To do that you can use the requestFromDatabase
custom command. This will send a request to the database and give you back the information. Using a .then
you can then design a test that takes the data into account. The code that does the interaction with the database is in the plugins
folder.
Once you have created your code and tests, it is time to push it. The husky
library will run all of the tests and some checks on your code to ensure it is up to the standards. In the .husky/pre-commit
file you can see what commands are run. If this fails, your commit will not have been created and you will need to fix the problems before trying again. Make sure that you add the files you changed to the commit before running again!
By doing this, you will get an earlier notification if something will not pass our CI/CD tests which will speed it all up. If you want to skip this check you can run git commit --no-verify
.
Whenever a PR is made in our github, the pipelines start working. In the .github/workflows
folder you will find some yml files that define actions github will take whenever a new commit is pushed to a PR. The client-code-style-check
, server-code-style-check
will run the linter on the code. The client-tests
and server-tests
will run the jest tests in their respective folders and the cypress-tests
will run the cypress tests headlessly. All of these have to pass for code to be allowed to be merged in! In the Actions
tab in github you can see the results of the run if there is something broken.
If the cypress tests fail and you are unsure why, the cypress job in the Actions
tab will have an artifact that has all of the videos of the run. You can download these to see the browser as it was running through your test.
Every PR will also have its own deployed version on heroku. In the PR there will be a link to this version so you can see how it runs in the deployed/production state. This is what the QA engineers can use to test the application.