Unit Testing on JS: Functional vs Imperative

Nowadays, JavaScript is all over the place. As the code evolved, testing libraries and strategies like TDD and BDD also did. Indeed, we all agree on the same thing: Code must be tested.

In the past years, a big part of the community shifted towards Functional programming. As a side effect, the paradigm shift brought us a more straightforward way of defining and writing unit tests.

So the thing is …

What is Functional programming?

Functional programming is a programming paradigm — a style of building the structure and elements of computer programs — that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data — wikipedia

Let's take a look at an example of code which will be the system under test:

const showResponseMessage = (response, source) => {
  let message = source.message;
  if(response.data.status) {
    message += response.data.status;
  }
  if(response.data.title) {
    message += " " + response.data.title;
  }
  return message;
}

Pretty simple, right? The function receives a response and a source, we compose the message based on data received on the response, and we return it. Let's test it!

describe('It should show the error message', ()=> {
  const response = {data: {status: 'fail', title: 'edit'}}
  const source = { message: 'The post action had '}
  it('Adds the extra info', ()=> {
    expect(showResponseMessage(response, status)).toEqual('The post action had fail edit')   
  })
})

Looks simple, right? Straight forward I'd say, let's improve it:

const showResponseMessage = (response, source) => {
  let messageResponse = getMessage(source);
  const {status, title} = response.data;
  messageResponse = addStatus(status, messageResponse);
  messageResponse = addTitle(status, messageResponse);
  return messageResponse;
}

const getMessage = source => source.message;

const addStatus = (status, message) => {
  return message + " " + status
}

const addTitle = (title, message) => {
  return message + " " + title 
}

I know, the number of code lines increased, at a glance, it just seems like a more sophisticated way of achieving the same thing. But, we increased the number of tests too, enhancing the overall quality assurance.

describe('It should show the error message', ()=> {
  const source = { message: 'The post action had'}
  const response = {data: {status: 'fail', title: 'edit'}}
  const {status, title} = response.data;

  it('gets the message from source', ()=> {
    expect(getMessage(source)).toEqual('The post action had')   
  })

  it('Adds Title properly', ()=> {
    let message = getMessage(source);
    expect(addTitle(title, message)).toEqual('The post action had  edit')
  })

  it('Adds Status properly', ()=> {
    let message = getMessage(source);
    expect(addStatus(status, message)).toEqual('The post action had  fail')
  })

  it('Adds returns the right message', ()=> {
    expect(showResponseMessage(response, source)).toEqual('The post   action had fail edit')
  })
})

In the first example, if we change the structure of the response and title, the test will fail, but we won't know why the test failed. Did it fail because of a missing response message or a wrong title retrieved as part of the response?

In the second example, we are testing each functionality separately. By assigning each function it's own responsibility, we make the code more maintainable, efficient, and versatile. For more information, check the single responsibility principle.

Let's consider the following scenario, suppose that our business logic changes, and we want to prevent the spaces on title and status. In such a case, we will have to create a function that receives a text and returns the sanitized version. To test that function, we have to create a test that solely asses that requirement; this test will not be related to showResponseMessage, but to space trimming.

That example points out a single case. However, as the logic and size of the application grow, the more relevant this approach to testing will become. When we start working with async calls, with the ability of composition and Higher-Order functions, we can make our "features" readable and our functions real units.

Furthermore, all of our original values are immutable. In the first example, we modified the message variable, now we don't, we still have our original message — And why is that important? Mutation means change, change adds complexity, opens the door for bugs, and sometimes makes things unpredictable.

When our code is predictable, self-explanatory, and easier to track and read, it's much easier to write cleaner tests.

"Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write a new code. ...[Therefore,] making it easy to read makes it easier to write." Robert C Martin.

So stay functional for simple testing and happier code reviews :)

Photo by Markus Spiske on Unsplash