How to refactor legacy JS to implement unit tests?

I have a WordPress site with a lot of JS files that were not structured to be tested - they were not written as modules that can be imported nor is there a app.js that loads them all as in a framework.

The files are only compiled and minified for use on the site, and I want to start redesigning little by little, as I am doing the maintenance of the site, adding tests for fixed bugs and new ones features.

All files have a structure similar to:

( function( window ) {
    'use strict';

    var document = window.document;

    var objeto = { 
        params : {
            // etc
        },
        init: function() {
            // etc
        },
        outroMetodo: function() {
            // etc
        }

    }

    objeto.init();
} )(this);

I was suggested to use Jest and the setup was pretty simple - the test environment is ready - but I don't know how to load the files that need to be tested. My current configuration in package.json is this:

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "verbose": true,
    "testMatch": [
      "<rootDir>/tests/jest/**/*.test.js"
    ]
  }
}

I imagine you need to refactor the files in some way to be able to load them in the Jest before running the tests, but how would the simplest way of allow this integration without rewriting the features? I tried to use the settings setupFiles and setupTestFrameworkScriptFile but since I don't have a single setup file it seems to me that it is not the ideal option.

Is there a way to include the file to be tested at the beginning of each test to test the methods?

include( '/arquivo.js' ); // pseudocodigo

describe("Testes para arquivo.js", function() {
    it("testes do metodo X", function() {
        expect(true).toBe(true);
    });
});
Author: Ricardo Moraleida, 2018-06-27

1 answers

You need four things to improve your code:

  1. a goal of how you wish your code had organized (if you started from scratch, how would you do it?)
  2. every new code you create must follow its ideal structure (according to step 1).
  3. as you tap on old code, you refactor it to fit the new architecture.
  4. when the old code is 80% -90% refactored, you make the other 10% -20% in one stroke only.

In JavaScript it is common for legacy code to be written as a script, which is a problem, because when importing the file for testing it will execute a lot of code(and this will happen only at the time of import).

To solve this problem, I suggest you remake your code to separate it into two parts: module and Script

Module is a piece of code where nothing is executed. In it you create classes / functions and exports them. Script is the type of code where you make modules run.

Example:

Module:

// module/auth.js
export default class Auth {
  authenticate(username, password) {
    // restante do código...
  }
}

Script:

// script/auth.js
import Auth from '../module/auth';

$(() => {
  $('form').on('submit', (evt) => {
    evt.preventDefault();

    const username = $('#username').val()
    const password = $('#password').val()

    const auth = new Auth()
    const result = await auth.authenticate(username, password);
    // restante do código...
  });
});

By doing this you can at least test module/auth.js, which is already a great start! If you want to go further, you can also set the jQuery part (assuming you are using jQuery) also in a "modscript":

Modscript:

// modscript/auth.js
import Auth from '../module/auth';

export default () => {
  $('form').on('submit', (evt) => {
    evt.preventDefault();

    const username = $('#username').val()
    const password = $('#password').val()

    const auth = new Auth()
    const result = await auth.authenticate(username, password);
    // restante do código...
  });
}

Script:

// script/auth.js
import execute from '../modscript/auth';
$(() => execute());

That way you are now also able to test interactions with the DOM. Since you already use Jest, then you already have JSDom installed and configured.

To test a "modscript" you do like this:

import execute from '../modscript/auth';

let originalAuthenticate;

beforeEach(() => {
  originalAuthenticate = auth.authenticate;
  auth.authenticate = jest.fn();
});

afterEach(() => {
  auth.authenticate = originalAuthenticate;
  $('body').html(''); // clean document
});

it('authenticates user upon form submission', () => {
  // Arrange
  $('body').append('<form><input id="username" /><input id="password" /></form>');
  $('#username').val('spam');
  $('#password').val('egg');
  auth.authenticate.mockResolvedValue(true);

  // Act
  execute();
  $('form').trigger('submit');

  // Assert
  expect(auth.authenticate.mock.calls).toContain(['spam', 'egg']);
  // outros expects...
});

Architecting modscripts tests are considerably more complex than architecting module tests, but they give more confidence in your implementations because the scope of the tests will increase greatly.

 1
Author: Eduardo Matos, 2019-07-30 13:35:29