When we commit code, it's important that our code doesn't have errors and does exactly what we expect it to, and if the code is publicly available (like on GitHub), it also matters how the code looks and that it is easy to read by others.
Code that behaves properly and isn't buggy
To prevent errors in our code and make sure our code behaves as we expect, we test our code with unit-testing/testing-libraries.
Luckily for us using React, it comes with a testing library that we can easily use and create tests with.
Readable and nice looking code
To make our code readable and nice to look at, we format our code by using spaces, linebreaks and tab indentation amongst others.
This can be automated for us with the use of an npm package called Prettier
(there are propably many others out there, but this is what we'll be using in this tutorial).
Doing it automatically before we commit
When testing, we must run the command npm test
and when we need to format our code, we must run npm run prettier
, but we must manually do this before every commit we make to make sure we don't commit wrong/error proned/ugly/hard to read -code.
Wouldn't it be great if we could do this automatically?
Guess what! We can... Wuhuu!
I will take you through a little journey where we will be looking at how to:
- Create tests in React
- Use prettier and set rules for formatting
- Create pre-commit hooks for prettier
- Create pre-commit hooks for tests
Creating a simple test
Start with a React project
In this tutorial we will use create-react-app
which (when installed) already includes a testing library ("@testing-library/react"
).
Start by creating a folder, named test-and-format
.
You can name it whatever you want, but make sure the name is all lowercase!
I use VSCode as my editor, but you can use whichever editor you prefer.
Open up VSCode with the test-and-format
folder as your project root.
Make sure the folder is completely empty, and then in the terminal, run:
npx create-react-app .
Create a simple component
I chose to make a simple Card
-component. So create a file named Card.js
and add this code inside:
function Card(){
return null;
}
export default Card;
This component does absolutely nothing yet (it only returns null). Don't worry, we will create the component when we have made our test.
Add Card to App
Clean up your App.js
so it looks something like this (also delete its dependencies):
import './App.css';
function App() {
return (
);
}
export default App;
import your Card
-component and return it:
import './App.css';
// Add the import
import Card from './Card';
function App() {
return (
// return the Card
<Card/>
);
}
export default App;
Create a simple test
Delete App.test.js
(because it will fail since we changed the content of App.js
).
Now we are ready to create our test for our Card
-component.
Create a file named Card.test.js
and add the following code:
// Import Reacts render and screen which is used by our test
import {render, screen} from "@testing-library/react";
// Import our Card -component
import Card from "./Card";
// The test itself
test("Checking if 'My Title' exists in the component", () => {
// We are rendering our component passing in a title
// and a text as props (the attributes)
render(<Card title="My Title" text="Something"/>);
// Parsing the "screen" for the text "my title"
// The "i" in the regular expressions means "ignore upper/lower-case"
var myTitle = screen.getByText(/my title/i);
// This is what we expect ("my title" to be in the document)
expect(myTitle).toBeInTheDocument();
});
Run npm test
to see if our test passes.
It will fail because our component isn't completed yet (remember, it returns null
!)
So let's finish it:
function Card({title, text}){
return (
<article className="Card">
<h1>{title}</h1>
<p>{text}</p>
</article>
);
}
export default Card;
Run npm test
again and see that our test now passes.
We have created this project with Test Driven Design (TDD) in mind, so we wrote the test first and then the component.
The idea with TDD is that we create our tests with specific criterias for the components first, and these criterias must then be met when we create our component.
This is to make sure we create a component that when the criterias are met, just works without flaws or problems that can break something further down the road, especially when working on a large project.
To illustrate this, let's pretend we made a small mistake when creating our component:
function Card({title, text}){
return (
<article className="Card">
// Here I forgot the curly braces around title:
<h1>title</h1>
<p>{text}</p>
</article>
);
}
export default Card;
When we now run our test with npm test
it will fail.
It fails because the actual text rendered is "title" and not "My Title" because "title" is hardcoded, but we created the component with props in mind and expected that the title
-prop contained the actual text: "My Title":
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.828 s
Ran all test suites.
npm ERR! Test failed. See above for more details.
We can now inspect what is wrong and (hopefully) find our little mistake, correct it, and run the test again to see that it now passes:
If we scroll up a bit in the terminal, we can see where the error happened:
4 | test("Checking if 'My Title' exists in the component", () => {
5 | render(<Card title="My Title" text="Something" />);
> 6 | var myTitle = screen.getByText(/my title/i);
| ^
7 | expect(myTitle).toBeInTheDocument();
8 | });
9 |
It fails on line 6 in our test, which means that the text "my title" was not found anywhere in the rendered component (whether lowercase or uppercase).
If we scroll up even further in the terminal, we see what is actually rendered:
<body>
<div>
<article
class="Card"
>
<h1>
title
</h1>
<p>
Something
</p>
</article>
</div>
</body>
And here we can see that the text "my title" is not anywhere in the markup (HTML).
Let's take a look at our component and see if we can spot what is wrong:
function Card({ title, text }) {
return (
<article className="Card">
<h1>title</h1>
<p>{text}</p>
</article>
);
}
export default Card;
Surely we can see that "title" is hardcoded, but our intention was to use the title prop, so let's add the curly braces and fix our little mistake:
function Card({ title, text }) {
return (
<article className="Card">
<h1>{title}</h1>
<p>{text}</p>
</article>
);
}
export default Card;
Let's run the test and see that everything works perfectly:
PASS src/components/Card.test.js
√ Checking if 'My Title' exists in the component (29 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.213 s
Ran all test suites.
This is all good, and we can test our components to see if they fail or pass.
Before we dig into pre-commits let's take a look at formatting our code with prettier (we ultimately also want our code to format nicely before we commit, right?).
Prettier
To format our code we use prettier and we need to install following packages:
- prettier
- eslint-config-prettier
The eslint-config-prettier
is needed for prettier to play nicely with ESLint.
It just disables unnecessary rules or rules that could conflict with Prettier
. React (create-react-app
) comes with ESLint pre-installed, so we need this package.
Install the packages with this command:
npm i -D prettier eslint-config-prettier
or
npm i --save-dev prettier eslint-config-prettier
Ignore files you don't want prettyfied
As default, Prettier will format all files in our project, so if there are any files we don't want Prettier to run through, we can define them in an ignore file.
Create a file named .prettierignore
and define files/folders that Prettier will ignore (it works just like .gitignore
if that is familiar to you):
Example content:
node_modules
build
coverage
.vscode
As an absolute minimium, you should add node_modules
to the ignore file, because the amount of files inside it is enormous, and it would take forever to run through them all (it is also unnecessary to prettify other developers code).
Configure Prettier to your likings
I want to ask you a couple of questions:
- Do you use spaces inside your brackets when destructuring?
- Do you use tabs or spaces when indenting?
- Do you use double (
"
) or single ('
) -quotes?
All of these things can be configured to make Prettier do all of these for you automatically.
How?
Create a file named .prettierrc.json
and add properties which defines the behaviour of Prettier (set the rules for formatting)
Example content (see a complete list of rules here):
{
"printWidth": 120,
"useTabs": true,
"semi": true,
"quoteProps": "consistent",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
}
The time has come for our pre-commit hooks (finally!)...
Run commands before a commit
What we wanted was to run both Prettier automatically and all our tests automatically, so we don't have to run npm run prettier
and then npm test
manually everytime we commit.
So let's take a look at how we can achieve this:
Prettier and the pre commit hook
The pre-commit hook enables you to run commands BEFORE a commit.
To enable the prettier before a commit, we must run this command in the terminal:
npx mrm lint-staged
This installs a package called husky
along with lint-staged
.
If we then add a property to scripts
in the package.json
file:
"prettier": "prettier --write ."
we can prettify all our files manually (according to our specifications in .prettierrc.json
) everytime we run this command in the terminal:
npm run prettier
Test before commit
To make our tests run:
We need a husky folder, which should ultimately contain our pre-commit hook for the tests. We create it with this command:
npx husky install
Then create a pre-commit file (with the pre-commit hook inside):
npx husky add .husky/pre-commit "npm test"
In my case npx husky add .husky/pre-commit "npm test"
did not work properly (it did not create the pre-commit file inside the husky folder, but instead gave me this message):
(if it worked for you, you can skip to the next section)
$ npx husky add .husky/pre-commit "npm test"
Usage
husky install [dir] (default: .husky)
husky uninstall
husky add <file> [cmd]
Examples
husky install
husky install .config/husky
husky add .husky/pre-commit
husky add .husky/pre-commit "npm test"
husky add .config/husky/pre-commit "npm test"
So to make it work, I had to create the file first:
npx husky add .husky/pre-commit
Then open the file (.husky/pre-commit
) and manually add npm test
on it's own line in the file:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm test
Add prettier to the commit file
Now, the only thing the pre-commit file does is run the npm test
command. We also want to run the prettier command (npm run prettier
), so let's add it:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run prettier
npm test
Make the commit actually commit when all tests pass
If at this point we try to commit something, the files will be prettyfied and our tests should run, but... the test will "hang" and never commit anything...
To fix this, we must do one more step. Install the cross-env package:
npm i -D cross-env
or
npm i --save-dev cross-env
and in package.json under scripts
we must change:
"test": "react-scripts test"
to
"test": "cross-env CI=true react-scripts test"
This will make sure that when we run the test (either by committing or with npm test
) the test will "break out" of its "wait state".
You can give it a try by running npm test
:
- with
"cross-env CI=true react-scripts test"
and
- with
"react-scripts test"
and see the difference for yourself.
What we have made
We have now succesfully created an automated feature where everytime we commit, our files are formatted nicely and consistently, and all tests are run:
if the tests pass: perform the commit
if the tests fail: commit will not be allowed!
This is what we want and if this works for you, congratulations, you now have functionality that makes sure you never commit "crappy" code (if your tests are created properly, that is).
Top comments (1)
Thanks, clearly post!