visit
Now, I honestly wouldn’t go back. When something breaks in a code base with 100% coverage, it is very likely your tests will tell you exactly where and how.
This isn’t to say unit testing is all you need. It isn’t. But leaving code untested is not a good option in my opinion either. Come back with me, to a time when I didn’t believe in the benefits of test coverage either.Is this a spy? I basically had no idea what any of this meant. Before I could be effective, the first thing to do was learn the language.
So, first thing’s first — the hell is a spy or a mock?
Although the first thing that comes to mind is James Bond or Ethan Hunt. That is definitely not what we are talking about here, though it isn’t a terrible metaphor. After reading some documentation I eventually learned that a spy is a function that has been modified by a testing framework to provide meta information about how it has been used. It spies on it. Kinda like how people could spy on you with Apple’s recent FaceTime Bug. So kinda like James Bond. A mock is similar to a spy but it has been modified even more. As well as providing and keeping track of how a particular function has been used, it also changes its behavior to be predictable. I also learned there are several types of testing. Not limited to the three most common: Unit Testing, Integration Testing, and E2E Testing. When we are “unit testing” that means we need to be able to break down our code into individual units. Anything outside of that particular unit is a candidate to be mocked, such as other functions or entire modules. Jest is my tool of choice for unit testing. Unit testing is the only type of testing where coverage is measured. When we are Integration Testing, we are testing the integration of our software with other pieces of software, such as a test that passes a message through Kafka that our service should receive, and that the result of that can be found in the database afterward. I also usually reach for Jest when creating Integration tests. E2E Testing is kinda like a bot using your app. You program it to load the site in a browser, click things, and ensure everything works as expected from a user’s perspective. Cypress is my favorite tool on this area, but that didn’t exist back when I was learning. Selenium was the big player of the day, and to be honest, it was a big enough domain I was happy to let a QA Automation Engineer handle that part. With new knowledge in hand now came the hard part: putting it to practice. I spent several months making sure every single piece of code I wrote had test coverage. At first, I admit, it was quite difficult. I spent a lot of time on StackOverflow looking up mocking and spying examples. By the end I found that I the amount of confidence I had in my code was substantially higher. Another benefit was when something broke my tests would usually tell me exactly where. When other engineers made changes to code that I made I could review it much more quickly. When important APIs changed, people were alerted via a failing test and either quickly updated it or gave their changes a second thought. More than that, I started writing better code. I learned that usually if something is hard to test, or hard to fully cover, it usually meant I didn’t write that code very well, and it could be refactored resulting in more maintainable and flexible APIs. To that end, trying to reach 100% coverage encouraged me to extract anonymous functions into named functions, and to understand partial application and dependency injection in many refactors. After getting integrations tests down as well, I even gave up GitFlow for trunk-based development. Committing to master was something I thought was crazy a few years back, and now I do it on a team of nearly 15 engineers every day.
I have a confession to make… I commit to master._I used to preach about Git Flow to keep my code releasable, rollback-able, and keep a clean history. But not anymore —…_gzht888.com
Enforcing Code Quality for Node.js_Using Linting, Formatting, and Unit Testing with Code Coverage to Enforce Quality Standards_gzht888.com
I figured the best way to demonstrate 100% coverage is showing how to get there. Throughout the journey we will likely discover several places where code can be refactored to be more testable. So, I’ll continue where I left off, and get coverage of this project to 100%, and show what refactors to make, where to use partial application and dependency injection, and what to mock along the way when coverage is difficult to get. So… Let’s get started. Here’s the project I’ll be working on:
The project has a react app in the app
folder, and a server
folder which contains the SSR logic. Let’s start with the application tests.
In the last article, after configuring Jest, I got started with a simple test for a simple component. I have several React components that are equally as simple.
This is one of the reasons that functional components are really powerful. Functions are easier to test than classes. They don’t have state — instead they have inputs and outputs. Given input X, they have output Y. When there is state it can be stored externally to the component. The new React Hooks API is nice in this regard because it encourages making functional components, and has an easily mockable mechanism to provide state to the component. Redux provides the same benefit in regards to testing. Let’s start by knocking out the rest of the simple components. We basically just need to render them and maybe check that some important pieces of info are rendered. I usually put code inline in the articles, but there’s not really anything new in these tests, so instead I’ve decided to link to the actual commits and only show one full example: Let’s take a look at the About page:
<div>This is the about page</div>
The next component, app/App.jsx
is slightly more complex. After writing a rendering test, you’ll notice there is still an unreachable anonymous function that is used in the Router to render the About page.
Now it is easy to test:
Because we have another set of tests for the About page above, we’ll leave its more specific tests to live there, and just need to check that it renders here.
And with that, the only file left to test in our application is app/client.js
, and then we can move on to finishing up server side tests.
The first thing I notice is that there is a reliance on global variables — document
, process
and module
. The second thing is that nothing is exported so it may be hard to run multiple times with different inputs.
We also will want to mock a few of the external modules: react-dom
, react-imported-component
, and app/imported.js
. Modules are a form of dependency injection themselves.
export const start = ({isProduction,document,module,hydrate}) => {const element = document.getElementById('app')const app = (<HelmetProvider><BrowserRouter><App /></BrowserRouter></HelmetProvider>)
// In production, we want to hydrate instead of render// because of the server-renderingif (isProduction) {// rehydrate the bundle marks from imported-components,// then rehydrate the react apprehydrateMarks().then(hydrate(app, element))} else {ReactDOM.render(app, element)}
const options = {isProduction: process.env.NODE_ENV === 'production',document: document,module: module,hydrate}
start(options)
Now we can actually access and test start with a variety of options as well as testing hydrate independently of the startup logic. The tests are a bit long, so I’ve put comments inline to explain what is going on. Here are tests for the file:
// mock module.hot
const module = {
hot: {
accept: jest.fn()
}
}
// mock options
const options = {
isProduction: false,
module,
document
}
start(options)
expect(ReactDOM.render).toBeCalled()
expect(module.hot.accept).toBeCalled()
})
it('hydrates when in production does not accept hot module reloads', () => {
const ReactDOM = require('react-dom')
const importedComponent = require('react-imported-component')
importedComponent.rehydrateMarks.mockImplementation(() => Promise.resolve())
// mock module.hot
const module = {}
// mock rehydrate function
const hydrate = jest.fn()
// mock options
const options = {
isProduction: true,
module,
document,
hydrate
}
start(options)
expect(ReactDOM.render).not.toBeCalled()
expect(hydrate).toBeCalled()
})
})
expect(typeof doHydrate).toBe('function')
doHydrate()
expect(ReactDOM.hydrate).toBeCalledWith(app, element)
})
})
})
Now when we run our tests, we should have 100% coverage of the app
folder, aside from app/imported.js
which is a generated file, and doesn’t make sense to test as it could generate differently in future version.
In jest.config
add:
Now when we run npm run test
we get the following results.
In the previous article I wrote tests for one application file, as well as one server file, so we for server/index.js
. Now we need to test the three remaining files in server/lib
.
Let’s start with server/lib/client.js
:
First off, I’ve noticed there’s a pretty big block of code that isn’t even used in the project from a previous abandoned strategy. Everything from export const parseRawHTMLForData
through const clientData
.
Next, server/lib/server.js
is quite tiny, so let’s knock that one out. Here is its code to refresh your memory, or if you’re just joining us now:
Finally, we have only one more file to test: server/lib/ssr.js
.
Here’s our ssr
module:
const sheet = new ServerStyleSheet()
const stream = sheet.interleaveWithNodeStream(
renderToNodeStream(sheet.collectStyles(app))
)
if (context.url) {
res.redirect(301, context.url)
} else {
const \[startingHTMLFragment, endingHTMLFragment\] = getHTMLFragments({
drainHydrateMarks: printDrainHydrateMarks()
})
res.status(200)
res.write(startingHTMLFragment)
stream
.pipe(
through(
function write (data) {
this.queue(data)
},
function end () {
this.queue(endingHTMLFragment)
this.queue(null)
}
)
)
.pipe(res)
}
Also write
and end
are a bit tough to get to, so we can pull those out higher using partial application as well.
const getApplicationStream = (originalUrl, context) => {const helmetContext = {}const app = (<HelmetProvider context={helmetContext}><StaticRouter location={originalUrl} context={context}><App /></StaticRouter></HelmetProvider>)
export function write (data) {this.queue(data)}
// partial application with ES6 is quite succinct// it just means a function which returns another function// which has access to values from a closureexport const end = endingHTMLFragment =>function end () {this.queue(endingHTMLFragment)this.queue(null)}
export const ssr = getApplicationStream => (req, res) => {try {// If you were using Apollo, you could fetch data with this// await getDataFromTree(app);
const context = {}
const stream = getApplicationStream(req.originalUrl, context)
if (context.url) {
return res.redirect(301, context.url)
}
const \[startingHTMLFragment, endingHTMLFragment\] = getHTMLFragments({
drainHydrateMarks: printDrainHydrateMarks()
})
res.status(200)
res.write(startingHTMLFragment)
stream.pipe(through(write, end(endingHTMLFragment))).pipe(res)
} catch (e) {log.error(e)res.status(500)res.end()}}
const defaultSSR = ssr(getApplicationStream)
export default defaultSSR
Here’s a link to look at the diffs in Github: , and . Now let’s write some tests. We’ll need to set the jest-environment for this file specifically for node otherwise the styled-components portion will not work.
expect(typeof doSSR).toBe('function')
doSSR(req, res)
expect(res.redirect).toBeCalledWith(301, '/redirect')
})
it('catches error and logs before returning 500', () => {
const log = require('llog')
const req = Object.assign({}, mockReq)
const res = Object.assign({}, mockRes)
const getApplicationStream = jest.fn((originalUrl, context) => {
throw new Error('test')
})
const doSSR = ssr(getApplicationStream)
expect(typeof doSSR).toBe('function')
doSSR(req, res)
expect(log.error).toBeCalledWith(Error('test'))
expect(res.status).toBeCalledWith(500)
expect(res.end).toBeCalled()
})
})
Finally, before wrapping things up, I’m going to make a small change to my jest.config
to enforce 100% coverage. Maintaining coverage is much easier than getting to it the first time. Many of the modules we tested will hardly ever change.
Part 1: Move over Next.js and Webpack 🤯_Simple Streaming Server Side Rendered (SSR) React + styled-components with Parcel_gzht888.com
Part 2: A Better Way to Develop Node.js with Docker_And Keep Your Hot Code Reloading_gzht888.com
Part 3: Enforcing Code Quality for Node.js_Using Linting, Formatting, and Unit Testing with Code Coverage to Enforce Quality Standards_gzht888.com
Part 5: A Tale of Two (Docker Multi-Stage Build) Layers_Production Ready Dockerfiles for Node.js_gzht888.com