13

Enforcing Code Quality for Node.js

 4 years ago
source link: https://hackernoon.com/enforcing-code-quality-for-node-js-c3b837d7ae17
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
YBJjQzJ.png!web

Using Linting, Formatting, and Unit Testing with Code Coverage to Enforce Quality Standards

Licensed from Adobe Stock Photo

If you are going to be writing code and shipping it to production, it’s important that the code is high quality.

Inmy last article, I showed you how to use docker-compose to leverage standardized, already existing Dockerfiles for development. The next step in getting our application ready for deployment is productionizing it.

I’m going to continue using the React/Parcel example from my earlier tutorial: Move over Next.js and Webpack!

Here is the source code: https://github.com/patrickleet/streaming-ssr-react-styled-components

I also haven’t done anything else related to getting the application “production ready”, so I’ll also talk about what is required for that, though it may take another article to finish… we’ll see how it goes. I’m ad-libbing this.

Let’s start with some quality control.

In this article we will explore Linting, Formatting, Unit Testing and Code Coverage and enforce some quality standards.

Linting & Formatting

According to Wikipedia, to “ Lint , or a linter , is a tool that analyzes source code to flag programming errors, bugs, stylistic errors, and suspicious constructs.”

This means it enforces things like using spaces vs. tabs or making sure your code using semicolons consistently or not.

There are probably a bunch of linting errors in my project currently.

My goal so far has to demonstrate particular concepts and having too many sidebars about different things really takes away from the concepts at hand. Therefore I chose to forgo linting to keep the previous articles focused.

Now that it’s time to “productionize” our app, quality is of increased priority.

My preferred linter format is StandardJS , which is a very minimalist setup. But before we set that up, let’s talk about formatting as well.

Formattingis similar to linting, but less focused on syntax errors and more focused on just making the code look prettier, hence the name of the popular package prettier.

Thanks to a couple of awesome open-source contributors on Github we can use them both in one package — prettier-standard . Thanks to Adam Stankiewicz , Kent C. Dodds , Adam Garrett-Harris , and Benoit Averty !

In the past I’ve written about using husky to make sure rules are run before each commit. The prettier-standard package also recommends doing so, so let’s add prettier-standard, husky, and lint-staged now.

Configuring prettier-standard

First install the required packages:

npm i --save-dev prettier-standard husky lint-staged

In package.json add the following “format” script and new “lint-staged” and “husky” sections:

{<br>  //...<br>  "scripts": {<br>    // ...<br>    <strong>"format": "prettier-standard 'app/**/*.js' 'app/**/*.jsx' 'server/**/*.js'"<br>  },<br>  "lint-staged": {<br>    "linters": {<br>      "**/*.js": [<br>        "prettier-standard",<br>        "git add"<br>      ],<br>      "**/*.jsx": [<br>        "prettier-standard",<br>        "git add"<br>      ]<br>    }<br>  },</strong><br>  // ...<br>}

I couldn’t get RegExp to work so without looking into the source I’m assuming it uses glob and not RegExp.

Now you can run npm run format to format your code and check for linting errors. Also, any time you try to commit, husky’s pre-commit hook will be called, which will make sure any staged files (git add stages files) are properly linted before allowing them to be commited.

Let’s see how I did on the first pass.

➜  npm run format
> [email protected] format /Users/patrick.scottgroup1001.com/dev/patrickleet/open-source-metarepo/stream-all-the-things<br>> prettier-standard 'app/**/*.js' 'app/**/*.jsx' 'server/**/*.js'
app/client.js 52ms<br>app/imported.js 11ms<br>app/styles.js 7ms<br>app/App.jsx 11ms<br>app/components/Header.jsx 76ms<br>app/components/Page.jsx 7ms<br>app/pages/About.jsx 6ms<br>app/pages/Error.jsx 5ms<br>app/pages/Home.jsx 6ms<br>app/pages/Loading.jsx 6ms<br>server/index.js 8ms<br>server/lib/client.js 11ms<br>server/lib/ssr.js 17ms

And basically every file except styles.js had linting errors or didn’t look pretty enough!

Ignoring files for linting and formatting

There is one small issue which is specific to this project — app/imported.js is a generated file, and should be ignored by the linter.

Although is has eslint-disabled at the top of the file, prettier does not know to enforce linting rules. No worries, let’s undo changes to that file, and then create a .prettierignore file and an .eslintignore file to explicitly ignore it from being formatted on future runs.

git checkout -- ./app/imported.js

Will undo changes to that file.

And now create .prettierignore and .eslintignore with the following lines:

app/imported.js<br>dist<br>coverage<br>node_modules

Now when running npm run format the file app/imported.js remains unchanged. Not addressing this could be problematic due to the fact the file is generated.

Finally, I’ve mentioned committing should also run npm run format as a pre-commit hook. Let’s try it out.

➜  git commit -m 'feat: prettier-standard'<br>husky > pre-commit (node v11.6.0)<br>  ↓ Stashing changes... [skipped]<br>    → No partially staged files found...<br>  ✔ Running linters...

Here’s the commit on Github .

Unit Testing and Code Coverage

As part of productionizing our application, we really should make sure our code is well-tested. Ideally, you should do this along the way, but I’m a bad person and have neglected it in this project thus far.

Let’s address that.

Installing and Configuring Jest

First, let’s install Jest for writing our unit tests.

npm i --save-dev jest babel-jest

Next, let’s add a jest config file so we configure jest to know where to find our files and be able to use pretty paths.

Add the following jest.json file:

{<br>  "roots": ["<rootDir>/__tests__/unit"],<br>  "modulePaths": [<br>    "<rootDir>",<br>    "/node_modules/"<br>  ],<br>  "moduleFileExtensions": [<br>    "js",<br>    "jsx"<br>  ],<br>  "transform": {<br>    "^.+\\.jsx?$": "babel-jest"<br>  },<br>  "<code class="markup--code markup--pre-code">transformIgnorePatterns": </code>["/node_modules/"],<br>  "coverageThreshold": {<br>    "global": {<br>      "branches": 10,<br>      "functions": 10,<br>      "lines": 10,<br>      "statements": 10<br>    }<br>  },<br>  "collectCoverage": true,<br>  "collectCoverageFrom" : [<br>    "**/*.{js,jsx}"<br>  ]<br>}

Alright, let’s unpack that. First, we set roots to <rootDir>/__tests__/unit. I like putting staging tests in __tests__/staging so setting the root to __tests__/unit will allow me to do that later on.

Next, we set modulePaths to the root directory, and node_modules. This way in our tests instead of using relative paths like ../../ we can just import app/* or server/*.

The next two keys are telling jest to use babel to load our files so things like import will work without issues.

And finally, the last three sections define coverage settings — the minimum thresholds, all at 10%, and where to collect coverage from. In this article I just aim to get the pieces configured. In the next one I’ll increase the coverage threshold to 100% and walk through that process of getting there.

And to run, we can define a test script in our package.json's scripts section. Because we are using babel-jest we will need to provide some babel settings as well, so we can set BABEL_ENV to test, and we will address that in the next section.

"scripts": {<br>  // ...<br>  <strong>"test": "cross-env BABEL_ENV=test jest --config jest.json",<br>  "test:watch": "cross-env BABEL_ENV=test jest --config jest.json --watch"</strong><br>}

Configuring Jest with Babel

First in order for the tests to work, we are going to need configure some babel settings. Add the following section in the env key of your .babelrc file:

{<br>  "env": {<br>    <strong>"test": {<br>      "presets":[<br>        ["@babel/preset-env"],<br>        ["@babel/preset-react"],<br>      ],<br>      "plugins": [<br>        ["@babel/plugin-syntax-dynamic-import"]<br>      ]<br>    },</strong><br>    // ...<br>  }<br>}

And let’s install the plugins and presets that we’ve referenced:

npm i --save-dev @babel/core @babel/preset-env @babel/preset-react @babel/plugin-syntax-dynamic-import babel-jest

We have 0% coverage currently, let’s add one test for the app, and one test for the server which should put us above our low threshold of 10%.

Testing the client side app with Enzyme

First, let’s test a file in app. We will want to shallow-render our components in app to test them. To do so, we will use enzyme.

npm i --save-dev enzyme <code class="markup--code markup--pre-code">enzyme-adapter-react-16</code>

Enzyme has a setup step that we must add before we can use it in our tests. In our jest.json file, add a new key:

{<br>//other settings<br>"setupTestFrameworkScriptFile": "<rootDir>/__tests__/setup.js"<br>}

And the setup file at __tests__/unit/setup.js:

import { configure } from 'enzyme';<br>import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

Now with Enzyme configured we can create __tests__/unit/app/pages/Home.jsx:

import React from 'react'<br>import { shallow } from 'enzyme'<br>import Home from 'app/pages/Home.jsx'
describe('app/pages/Home.jsx', () => {<br>  it('renders style component', () => {<br>    expect(Home).toBeDefined()<br>    const tree = shallow(<Home />)<br>    expect(tree.find('Page')).toBeDefined()<br>    expect(tree.find('Helmet').find('title').text()).toEqual('Home Page')<br>    expect(tree.find('div').text()).toEqual('Follow me at <a href="http://twitter.com/patrickleet" title="Twitter profile for @patrickleet" target="_blank">@patrickleet</a>')<br>    expect(tree.find('div').find('a').text()).toEqual('<a href="http://twitter.com/patrickleet" title="Twitter profile for @patrickleet" target="_blank">@patrickleet</a>')<br>  })<br>})

Seems how our component is just a single function, that’s all we need to reach 100% coverage for this file.

Server side tests

For good measure, let’s create a server side test as well.

I want to test server/index.js but before I do, there are a couple of refactors that will make our lives a little bit easier.

Unit tests are meant to test a single unit. That means even though our app is using express, we are not testing express as part of this unit test. We are testing that our server is configured with the appropriate routes, middleware, and that listen is called to start the server. Unit Tests for express belong in the express project.

In order to only test the single unit we care about, we can use mocking to create lightweight interfaces that we can track using jest.mock. If we extract the server instantiation out of index into it’s own file, we will be able to more easily mock the server.

Create the file server/lib/server.js with the following contents:

import express from 'express'
export const server = express()<br>export const serveStatic = express.static

And update server/index.js like so:

import path from 'path'<br>import log from 'llog'<br><strong>import { server, serveStatic } from './lib/server'</strong><br>import ssr from './lib/ssr'
// Expose the public directory as /dist and point to the browser version<br><strong>server</strong>.use(<br>  '/dist/client',<br>  <strong>serveStatic</strong>(path.resolve(process.cwd(), 'dist', 'client'))<br>)
// Anything unresolved is serving the application and let<br>// react-router do the routing!<br><strong>server</strong>.get('/*', ssr)
// Check for PORT environment variable, otherwise fallback on Parcel default port<br>const port = process.env.PORT || 1234<br><strong>server</strong>.listen(port, () => {<br>  log.info(`Listening on port ${port}...`)<br>})

Now in our test we can simply mock server/lib/server.js instead of a more complex mock of express.

Let’s create the test at __tests__/unit/server/index.js:

import 'server/index'
jest.mock('llog')<br>jest.mock('server/lib/server', () => ({<br>  server: {<br>    use: jest.fn(),<br>    get: jest.fn(),<br>    listen: jest.fn()<br>  },<br>  serveStatic: jest.fn(() => "static/path")<br>}))<br>jest.mock('server/lib/ssr')
describe('server/index.js', () => {<br>  it('main', () => {<br>    const { server, serveStatic } = require('server/lib/server')<br>    expect(server.use).toBeCalledWith('/dist/client', "static/path")<br>   expect(serveStatic).toBeCalledWith(`${process.cwd()}/dist/client`)<br>    expect(server.get).toBeCalledWith('/*', expect.any(Function))<br>    expect(server.listen).toBeCalledWith(1234, expect.any(Function))<br>  })<br>})

If we run the coverage now, we will notice that the coverage for server/index.js is not 100%. We have an anonymous function passed to listen which is difficult to get at. This calls for some minor refactoring.

Refactor the listen call to extract the anonymous function.

export const onListen = port => () => {<br>  log.info(`Listening on port ${port}...`)<br>}<br>server.listen(port, onListen(port))

Now we can easily test onListen.

Let’s add another test to our server/index.js suite to account for it.

import <strong>{ onListen } from</strong> 'server/index'
// ...
describe('server/index.js', () => {<br>  // ...
<strong>it('onListen', () => {<br>    const log = require('llog')<br>    onListen(4000)()<br>    expect(log.info).toBeCalledWith('Listening on port 4000...')<br>  })</strong><br>})

And with that, we have 100% coverage for server/index.js as well as app/pages/Home.jsx.

With our two tests we’ve managed to increase our coverage from 0% to 35–60% depending on the metric:

Current CoverageAdd testing to pre-commit hooks

Lastly, we want to only make sure that tests as a pre-commit hook as well to prevent broken tests from making it into the code, and later on, any untested code.

In package.json change pre-commit to:

"pre-commit": "lint-staged && npm run test"

Now when someone tries to commit to the project it will enforce the coverage standards defined in your Jest config as well as making sure all the tests pass!

Conclusion

When it comes time to get your application production ready, it is imperative to enforce quality standards in an automated way. In this article I showed you how to set up and configure tools for linting and formatting your code, as well as how to configure your project for testing using enzyme and jest with enforced code coverage.

In the next part we will increase coverage to 100% before continuing on with creating a production ready Dockerfile.

As always, if you’ve found this helpful, please click and hold the clap button for up to 50 claps, follow me, and share with others!

Check out the other articles in this series! This was part 3.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK