Best way to test IIFE (Immediately Invoked Function Expression)
Asked Answered
V

2

6

So I have an existing application which uses IIFEs extensively in the browser. I'm trying to introduce some unit testing into the code and keep with the pattern of IIFE for new updates to the code base. Except, I'm having trouble even writing a test which gives me a handle to the code. For example I see this type of logic all over the code base:

var Router = (function (router) {

   router.routeUser = function(user) {
      console.log("I'm in! --> " + user)
   };

   return router;
})(Router || {});

Then the JS file is included in a script tag in the markup:

<script src="js/RouteUser.js"></script>

and called like this in the production code:

Router.routeUser(myUser)

So my question is, how do I write a test which tests the method routeUser? I've tried this in my Mocha Test:

var router = require('../../main/resources/public/js/RouteUser');

suite('Route User Tests', function () {
    test('Route The User', function () {
        if (!router)
            throw new Error("failed!");
        else{
            router.routeUser("Me")
        }
    });
});

But I get an exception:

TypeError: router.routeUser is not a function
at Context.<anonymous> (src\test\js\RouteUser.test.js:8:20)

Then I tried returning the method, which gives the same error:

var Router = (function (router) {
    return {
        routeUser: function (user) {
            console.log("I'm in! --> " + user)
        } 
    }
}
)(Router || {});

Can anyone point me the right direction here?

Verditer answered 9/11, 2017 at 14:40 Comment(4)
you're trying to require this file, does it have any module.exports?Kinin
No I don't have exports. I suppose I could try adding it, but I'm trying not to alter the existing patter in my production code. Aren't module.exports only for Node? In this case the code is run in the browser so isn't technically Node, so I am shying away from module.exports. Thoughts?Verditer
You mentioned Mocha and even used require() in your code snippet. This already kind of suggests that you want to run your unit tests using Node. But yeah, Mocha can run in a browser too - which is it?Kinin
I'm going to try and run these tests in the browser for now. I didn't even know this was possible, and it would help me greatly as I wouldn't have to alter existing code I think.Verditer
C
1

If you intend to run your Mocha tests in the browser, you do not have to alter your existing code.

Let's walk through the IIFE pattern, because based on your code, I think you may misunderstand how it works. The basic shape is this:

var thing = (function() {

   return 1;

})();

console.log(thing) // '1'

It's a var declaration setting thing equal to the value on the right side of the equals sign. On the right, the first set of parens wraps a function. Then, a second set of parens sits right next to it, at the end. The second set invokes the function expression contained in the first set of parens. That means the return value of the function will be the right-side value in the var statement. So thing equals 1.

In your case, that means that the outer Router variable is set equal to the router variable returned by your function. That means you can access it as Router in your tests, after including the script in the DOM:

suite('Route User Tests', function () {
    test('Route The User', function () {
        if (!Router) // <- Notice the capital 'R'
            throw new Error("failed!");
        else {
            Router.routeUser("Me") // <- capital 'R'
        }
    });
});

If you intend to run your tests with node, see Kos's answer.

Capybara answered 9/11, 2017 at 17:14 Comment(1)
Interesting. And thanks for the explanation about IIFEs. I hadn't thought of running my test in the browser, which indeed would mean I would have access to Router in the same way as my production code today. In fact it makes a lot of sense running the tests like this because it mimics how the code is in fact run in production. Thanks.Verditer
K
6

It sounds that...

  • you have a codebase of scripts that are only used in the browser context (usage of IIFE suggests this)
  • you'd like to introduce browserless unit tests (Jest, Mocha?) using node.js (good idea!)
  • but you probably don't want to migrate the whole codebase to a different coding style at this moment in time (can be a lot of work depending on the size of your codebase)

Given these assumptions, the problem is that you want your code to...

  • act as a script when used on production (set global window.Router etc)
  • act as a module when used in unit tests so that you can require() it in unit tests

UMD

UMD, or universal module definition, used to be a common way to write code so that it can work in multiple environments. Interesting approach, but very cumbersome, and I like to think UMD is a thing of the past nowadays...

I'll leave it here for completeness.

Just take UMD's idea

If the only thing you want for now to make a specific script act as a module too, so that it's importable in tests, you could do a small tweak:

var Router = (function (router) {

   router.routeUser = function(user) {
      console.log("I'm in! --> " + user)
   };

   if (typeof exports === "object") {
      module.exports = router;
      // now the Mocha tests can import it!
   }
   return router;
})(Router || {});

Long term solution

In the long run, you can get lots of benefits by rewriting all your code to use ONLY modules and use a tool like webpack to package it for you. The above idea is a small step in your direction that gives you one specific benefit (testability). But it is not a long term solution and you'll have some trouble handling dependencies (what if your Router expects some globals to be in place?)

Kinin answered 9/11, 2017 at 17:4 Comment(3)
Wow worked thanks! I'm unclear what "typeof exports === "object" means though. I've never defined "exports" in my IIFE. Where does it come from? Also, for module.exports, where is module defined? As far as the long term solution, to rewrite all code to use ONLY modules. I suppose that I consider Router above to be a module in itself, as it is an isolated piece of code which I can call using Router.routeUser() in the browser. What do you mean by a Module? When you say this do you mean that Router should always have module.exports, even if typeof exports != "object"?Verditer
By modules I mean CommonJS modules - source files that can import stuff via require() and export stuff via assigning to a module.exports. Node (and mocha) has CommonJS by default, but it's also understood by build tools like Webpack. This makes it easy to re-use modules between the browser and Node if they're in CommonJS. (There are also ES6 modules that use import and export keywords.)Kinin
typeof exports === "object" is just a check if there's a symbol "exports" defined in scope - if there is, we assume the code is running in CommonJS context. You want the same code to run in the browser without using build tools for now, right? This is why we need to check which environment we're inKinin
C
1

If you intend to run your Mocha tests in the browser, you do not have to alter your existing code.

Let's walk through the IIFE pattern, because based on your code, I think you may misunderstand how it works. The basic shape is this:

var thing = (function() {

   return 1;

})();

console.log(thing) // '1'

It's a var declaration setting thing equal to the value on the right side of the equals sign. On the right, the first set of parens wraps a function. Then, a second set of parens sits right next to it, at the end. The second set invokes the function expression contained in the first set of parens. That means the return value of the function will be the right-side value in the var statement. So thing equals 1.

In your case, that means that the outer Router variable is set equal to the router variable returned by your function. That means you can access it as Router in your tests, after including the script in the DOM:

suite('Route User Tests', function () {
    test('Route The User', function () {
        if (!Router) // <- Notice the capital 'R'
            throw new Error("failed!");
        else {
            Router.routeUser("Me") // <- capital 'R'
        }
    });
});

If you intend to run your tests with node, see Kos's answer.

Capybara answered 9/11, 2017 at 17:14 Comment(1)
Interesting. And thanks for the explanation about IIFEs. I hadn't thought of running my test in the browser, which indeed would mean I would have access to Router in the same way as my production code today. In fact it makes a lot of sense running the tests like this because it mimics how the code is in fact run in production. Thanks.Verditer

© 2022 - 2024 — McMap. All rights reserved.