Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Functional Programming Unit Testing in Node (Part 3)

DZone's Guide to

Functional Programming Unit Testing in Node (Part 3)

Learn to navigate class-based code, compose asynchronous functions, and define dependencies in curried functions to make them easier to test.

· Web Dev Zone ·
Free Resource

Jumpstart your Angular applications with Indigo.Design, a unified platform for visual design, UX prototyping, code generation, and app development.

Welcome to Part 3 where we'll show you how to navigate class based code using FP, go over composing all these asynchronous functions we wrote, and continuing to define our dependencies in curried functions to make them easier to test.

Contents

This is a six-part series on refactoring imperative code in Node to a functional programming style with unit tests. You are currently on Part 3.

The use of nodemailer is tricky. It's a library that sends emails in Node. However, it uses a lot of classes and instances to do so which makes even just stubbing a pain, but we'll make it work. We'll tackle the Object creation stuff first since factory functions, functions that simply return an Object based on input data, are easier to write, compose, and test.

Simple Object Creation First

For this transport object:

const transporter = nodemailer.createTransport({
    host: emailService.host,
    port: emailService.port,
    secure: false,
})

We'll take out the Object part, and just create that as a pure factory function:

const createTransportObject = (host, port) => ({
    host,
    port,
    secure: false,
})

And then test:

describe('createTransport when called', () => {
    it('should create a host', () => {
        expect(createTransportObject('host', 'port').host).to.equal('host')
    })
    it('should crate a port', () => {
        expect(createTransportObject('host', 'port').port).to.equal('port')
    })
})

Next up, the mailOptions Object:

const mailOptions = {
    from: emailService.from,
    to: emailService.to,
    subject: emailService.subject,
    html: emailBody,
    attachments: attachments,
}

We'll just convert to a function:

const createMailOptions = (from, to, subject, html, attachments) =>
    ({
        from,
        to,
        subject,
        html,
        attachments
    })

And do a single test:

describe('createMailOptions when called', () => {
    it('should create an Object with from', () => {
        expect(createMailOptions('from', 'to', 'subject', 'html', []).from)
            .to.equal('from')
    })
})

Last one is the error. We'll take this:

...
if (err) {
    err.message = 'Email service unavailable'
    err.httpStatus
...

And convert to another factory function:

const getEmailServiceUnavailableError = () => new Error('Email service unavailable') 

And the test:

describe('getEmailServiceUnavailableError when called', () => {
    it('should create an error with a message', () => {
        const error = getEmailServiceUnavailableError()
        expect(error.message).to.equal('Email service unavailable')
    })
})

Routine by now, right? Give some inputs, test the output, stub if you need to. Eventually, these functions can be combined together either synchronously or asynchronously.

Dem Crazy Classes

Now it's time to wrap the nodemailer class creation. We'll take the createTransport:

const transporter = nodemailer.createTransport({
    host: emailService.host,
    port: emailService.port,
    secure: false,
})

And make it pure (we already took out the transport object creation):

const createTransportMailer = curry((createTransportFunction, transportObject) =>
    createTransportFunction(transportObject))

And the test:

describe('createTransportMailer when called', () => {
    it('should work with good stubs', () => {
        expect(createTransportMailer(stubTrue, {})).to.equal(true)
    })
})

Not so bad. Now let's tackle the actual send email. We'll take the existing callback:

transporter.sendMail(mailOptions, (err, info) => {
    if (err) {
        err.message = 'Email service unavailable'
        err.httpStatusCode = 500
        return next(err)
    } else {
        return next()
    }
})

And remove the next, and convert to a pure Promise-based function:

const sendEmailSafe = curry((sendEmailFunction, mailOptions) =>
    new Promise((success, failure) =>
        sendEmailFunction(mailOptions, (err, info) =>
            err ?
            failure(err) :
            success(info)
        )
    )
)

As before, you pass the actual function that sends the email and we'll call it. This allows your real code to pass in the nodemailer's transport.sendEmail, and unit tests a simple function.

describe('sendEmailSafe when called', () => {
    const sendEmailStub = (options, callback) => callback(undefined, 'info')
    const sendEmailBadStub = (options, callback) => callback(new Error('dat boom'))
    it('should work with good stubs', () => {
        return expect(sendEmailSafe(sendEmailStub, {})).to.be.fulfilled
    })
    it('should fail with bad stubs', () => {
        return expect(sendEmailSafe(sendEmailBadStub, {})).to.be.rejected
    })
})

How we lookin'?

Supa-fly. Let's keep going.

Compose in Rows

Now that we have tested functions, we can start composing them together. However, that'd kind of defeats the purpose of "refactoring a moving target" which is what's helpful to more people.

Meaning, often you'll work on codebases that you didn't create, or you did, but you're still struggling to keep all the moving pieces in your head as requirements change. How can you positively affect them without changing too much of the interfaces? It's a skill that's learned with practice.

So let's practice together! We've already looked at the function, identified the pieces that need to be pure, visualized (sort of) how they fit together in an imperative-like order, and unit tested them (mostly) thoroughly.

Peace Out Scope

Let's tidy the place up first. This:

function sendEmail(req, res, next) { 

to this to ensure no need for this:

const sendEmail = (req, res, next) => { 

We'll nuke that { into orbit when we're done, for now he can chill.

Saferoom

The rule for JavaScript is that as soon as something is async (meaning your function/closure uses a callback or Promise), everything is async. The Promise is flexible in that you can return a value or a Promise and the rest of the .then and .catch will work.

One feature that the FP developers love is that it has built-in try/catch.

const backup = () => new Promise(success => {
    console.log("hey, about to boom")
    throw new Error('boom')
})
backup()
    .then(() => console.log("won't ever log"))
    .catch(error => console.log("it boomed:", error))

The bad news is that SOMEONE has to .catch or you'll get an uncaught exception. In Node that's the unhandledRejection event, and in the browser the window.onunhandledrejection event. Worse, a lot of async/await examples exclude the try/catch, completely ignoring that errors can happen, accidentally encouraging impure, error prone functions.

Eventually you'll want to look into never allowing a Promise to throw/catch, and instead return an ADT. You can read more about doing that with a video example using async/await as well.

Start With a Promise

We fixed the function to be an arrow function to remove any care about scope and this. The function has inputs. What it is missing is an output. You'll notice they return next, but the next function is a noop, a method that returns undefined. It doesn't return any value and is considered a "no operation," which we also call "something that intentionally creates side effects," similar to console.log. In the console's case, it shoves text in, standard out.

Since it's an async function, let's return a Promise, and gain the error handling benefits as well. We'll change the signature from this:

const sendEmail = (req, res, next) => {
const files = req.files
...

To this:

const sendEmail = (req, res, next) => {
        return new Promise((success, failure) => {
                    const files = req.files
...

Don't worry, we'll get rid of the return and the second { later on. The good news at this point is we could unit test sendEmail in a functional way by giving it some inputs, and checking what it outputs. The first test would output a Promise. The second would output to undefined for now because of next.

However, as you can see we still need a lot of mocks because none of the four dependencies like userModule.getUserEmail, fs.readFile, config.get, and nodemailer.createTransport are an input to the function. Let's remove the need for mocks right now.

const sendEmail = curry((readFile, config, createTransport, getUserEmail, req, res, next) => 
... 

Now you know the dark secret of Unix and Functional Programming: "It's someone else's problem to deal with state/supplying dependencies higher up the chain." We're high up the chain, it's our responsibility, and suddenly FP doesn't feel so off the chain, eh?

Currying Options

Before we talk about how to make this easier to deal with, let's talk about the order of the parameters which is intentional to make currying more useful. This is also my opinion around order, so feel free change the order as you see fit.

You DO NOT have to use curry or use partial applications. It's just useful in functional programming because you'll often have a lot of parameters like this where many of them are known ahead of time, so you just make it a habit. It can also help reduce the verbosity in using pure functions and your unit tests. It also makes composing easier because sometimes you'll already know a few of the parameters ahead of time.

Left: Most Known/Common, Right: Less Known/Dynamic

The overall goal with curried functions is to put most known ahead of time dependencies to the left, and the most unknown things to the right.

Reading files in Node via fs is commonly known since it's built into Node. So that's first.

The config library in Node is a common library often at the core of how your app handles different environments and configurations. So that's a tight second.

The nodemailer's createTransport function is third since there aren't that many options to send emails in Node, but it's still a library unlike fs.

The getUserEmail is our function that accesses a third party service that gets the user information so we can get their email address. We snag this from their session ID. This is not a well known library, nor a built-in function to Node, it's something we built ourselves and could change, so it's fourth.

The req is the Express Request object, the res if the Express Response object, and the next is the connect middleware function.

Hopefully your Spidey Sense is tingling, and you immediately say to yourself, "Wait a minute, this is an Express application, the req/res/next parameters are super well known; why aren't they first, or at least third?" The answer is yes and no.

Yes, this function is currently an Express middleware function, and is expected to have one of the two signatures: (req, res, next) for middleware, and (err, req, res, next) for error handlers.

No, in that we'd never give a function to an Express route without concrete implementations. Meaning, we don't expect Express to somehow magically know we need a config, nodemailer, etc. We'll give those, like this:

app.post('/upload', sendEmail(fs.readFile, config, nodemailer.createTransport, user.getUserEmail)) 

And now you see why; the Express request, response, and next function are actually the most dynamic parts. We won't know those until the user actually attempts to upload a file, and Express gives us the request, response, and next function. We just supply the first four since we know those.

WARNING: Beware of putting curried functions into middlewares. Express checks the function arity, how many arguments a function takes, to determine which type of middleware it should make: three for middleware, four for an error handler. They check function arity via Function.length. Lodash's curried functions always report 0. Sanctuary always reports one because of their "functions should only ever take one parameter" enforcement. Ramda is the only one that retains function arity. Since Express only cares about errors, you're safe putting (req, res, next) middlewares with a 0 or 1 function arity. For errors, you'll have to supply old school functions, or a wrapper that has default parameters that default to concrete implementations.

Knowing the limitations, we'll be fine, so let's use Lodash' curry.

Start the Monad Train... Not Yet

Let's start the Promise chain by validating the files. However, that pesky next function adds a wrinkle:

const files = req.files
if (!Array.isArray(files) || !files.length) {
    return next()
}

The entire function needs to be aborted if there are no files to email. Typically Promises, or Eithers, operate in a Left/Right fashion. A Promise says, "If everything's ok, we keep calling thens. If not, we abort all thens and go straight to the catch." An Either works about the same way: "If everything is ok, we return a Right. If not, we return an Left bypassing all Rights we find." This is because like Promises, you can chain Eithers.

However, there's no way to "abort early." If you go back to an async/await style function, you can write it in imperative style and abort early. We're not in the business of creating imperative code, though. For now, we'll just use a simple ternary if to determine if we should even go down the email route.

const sendEmailOrNext = curry((readFile, config, createTransport, getUserEmail, req, res, next) =>
    validFilesOnRequest(req) ?
    sendEmail(readFile, config, createTransport, getUserEmail, req, res, next) :
    next() && Promise.resolve(false))

Note two important things here. We only run the sendEmail function if we even have files to process. Second, since next is a noop, we can ensure the Promise.resolve(false) will return the resolved Promise with the resolved in it. This allows the next to inform Express that this middleware has completed successfully, AND return a meaningful value; false for not sending the email.

Ok, NOW Start the Monad Train

We can now nuke the Array checking. From this:

...
return new Promise((success, failure) => {
            const files = req.files
            if (!Array.isArray(files) || !files.length) {
                return next()
            }
            userModule.getUserEmail(req.cookies.sessionID).then(value => {
...

To this:

...
return new Promise((success, failure) => {
return userModule.getUserEmail(req.cookies.sessionID).then(value => {
...

Great, but notice we have now a Promise wrapped in a Promise. Let's refactor now that we're clear. From this:

const sendEmail = curry(readFile, config, createTransport, getUserEmail, req, res, next) => {
        return new Promise((success, failure) => {
                    return userModule.getUserEmail(req.cookies.sessionID).then(value => {

To:

const sendEmail = curry((readFile, config, createTransport, getUserEmail, req, res, next) =>
        userModule.getUserEmail(req.cookies.sessionID).then(value => {

Get Busy Child!

The Gets

One problem, though, is with the accessing of the cookie. Express, using the cookie middleware plugin, is the one who adds the .cookies Object to the request object. Even so, the cookie might not have been sent from the client. Worse, it's a "dot dot." Yes, we "know" req here is fine, and yes know "know" req.cookies is fine because we imported the module and told Express to use the middleware.

That's not the point. We're creating pure functions, and getUserEmail is the one whose responsibility is to validate the cookie value. If you can prevent creating null pointers, you're well on your way.

Hopefully at this point, again, your Spidey Sense is tingling, wondering why don't you first validate the cookie's value before you even run this. You should, and if you did, you'd be well on your way to creating a total function that can handle the variety of types and data, or lack thereof. A Maybe would be better because we'd be forced to deal with receiving a Nothing. We'll keep this pragmatic, and assume another function will handle informing the client that they are missing a sessionID cookie. However, we're certainly not going to allow that to negatively affect our functions purity.

For now, just a simple get to make it safe.

userModule.getUserEmail(get('cookies.sessionID', req).then(value => { 

Take a look at the Indigo.Design sample applications to learn more about how apps are created with design to code software.

Topics:
web dev ,node.js ,unit testing ,web application development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}