We're planting a tree for every job application! Click here to learn more

FP Unit Testing in Node - Part 3: OOP, Compose, and Curry

Jesse Warden

15 Jun 2021

•

10 min read

FP Unit Testing in Node - Part 3: OOP, Compose, and Curry
  • Node.js

OOP, Compose, and Curry

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.

Class Wrangling

The use of nodemailer is tricky. It's a libray 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 because I'm at the pool with my girls and lazy right meowwww, omg dat sun is amaze:

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'?

Screen-Shot-2018-06-11-at-7.06.57-PM-173x300.png

Supa-fly. Let's keep going.

Compose in Dem 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 an 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", similiar to console.log. In 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.

Define Your Dependencies

However, as you can see we still need a lot of mocks because none of the 4 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 meow.

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' 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 times, 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 put the 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 3rd 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 3rd 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 ourself and could change, so it's 4th.

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 3rd?" The answer is yes and no.

Yes, this function is currently an Express middleware function, and is expected to have 1 of the 2 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 4 since we know those.

WARNING: Beware 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: 3 for middleware, 4 for an error handler. They check function arity via Function.length. Lodash's curried functions always report 0. Sanctuary always reports 1 because of their "functions should only ever take 1 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 paramteres 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 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 2 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!

Dem Gets

One problem, though, 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 responsiblity 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 in 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 pragmmatic, 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 => {
Did you like this article?

Jesse Warden

Software @ Capital One, Amateur Powerlifter & Parkourist

See other articles by Jesse

Related jobs

See all

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

WorksHub

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
hello@works-hub.com

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

Join over 111,000 others and get access to exclusive content, job opportunities and more!

© 2024 WorksHub

Privacy PolicyDeveloped by WorksHub