DEV Community

loading...
Cover image for Testing and formatting before commit

Testing and formatting before commit

Sune Seifert
I'm a student at Roskilde Teknisk Skole where I am studying to become a "junior" web developer.
Updated on ・8 min read

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:

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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 |
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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).

Discussion (0)