Avoiding Global State in JavaScript and the Testing Thereof

Testing your JavaScript can be painful. Managing the various kinds of global state seems to be the best way to protect yourself from mayhem. We'll explore those types and how you can deal with each.
_The examples below are written in CoffeeScript and Jasmine. I use explicit return statements to try and make it more clear to those of you not as familiar with CoffeeScript._ Global state is bad. We all know it, but we seem to have forgotten how to recognize it in JavaScript. There are many forms, as we'll see. When your code accesses global state, you have to pay extra attention to avoid test pollution by saving state before and resoring it after test runs. Check out this example: If we instead pull the global state access up, we can ignore it completely when we test this method. If you continue to do this, you start to see exactly where your functions' dependencies lie. If your argument list is growing too large, it's not because you decided to eliminate global state, it's because your function has too many dependencies. This refactoring simply exposed that problem to you. h2. Avoiding Global State: Cookies Tests should not set cookies unless you are testing a cookie library. You should mock out the cookie access and test that your methods call the cookie library or method properly. You can also always check the cookies in the console document.cookie or with the Edit This Cookie Chrome extension. Below are examples on how to stub this. h2. Avoiding Global State: DOM Accessing the DOM is not always necessary. In fact, the instances where this is actually required are quite rare, if you structure your app the right way. In my current app, all specs (except a few) never touch the DOM via #jasmine_content (or any other part). The trick is that DOM fragments act (mostly) the same if they are attached to the window.document or not. You can even fire events on DOM fragments and waitsFor them to be triggered. If you absolutely must insert items in the DOM, use #jasmine_content. Make sure you clean up after yourself as well. Below is an example: h2. Avoiding Global State: Network There are only two reasons that your tests should ever access the network: the need to load a fixture or a local script. Even then, that should point to localhost and it should succeed. Anything else should be stubbed out. This includes third party scripts hosted on third party sites, such as google maps and facebook. You can always stub those libraries. Essentially, if the build is run on a box without an internet connection, it should still succeed. Sometimes actions can cause a network request without you realizing it. One such example is inserting an image tag with a src attribute into the DOM. It's important to check the network tab to make sure that nothing new shows up there. Below are examples of ways to mock out network access: For images that aren't what you might call figures, consider setting them with css as a background image instead. Beyond the semantic benefits, this will prevent those images from being loaded in a test environment where you don't load your CSS. h2. Avoiding Global State: Other Scroll Position, Window Size, and Local Storage are also forms of global state. Make sure that you are properly mocking these calls out as well. These can be especially annoying if you run your javascript specs in a browser because the viewport will change on you seemingly randomly. h2. Mocking Dependencies Dependencies should be mocked. Assertions should be made that those dependencies were called properly. That's what makes them unit tests. Allowing execution to pass into a dependent module turns your spec into an integration test. Integration tests are useful, but you should do so explicitly and with purpose. Some might say that people mock too often--that the fact that their code has multiple depedencies is an indication that you need to refactor. I definitely agree with that. The point here is that if you have depedencies, you need to handle them properly. Sometimes that's with a mock and sometimes it's with a refactoring. In my travels, I discovered a series of specs where the execution flow of each one went: Module Under Test > Dependent Module > Sub-dependent Module > DOM [insert] > [trigger] Network Request. Then, the assertion tested that the DOM triggered the Network request properly. Ideally, there would be unit tests all along the way. Even an integration test still shouldn't trigger a network request. Below is an example of how a dependent module can have untested side effects: h2. Using the Module Pattern The pervasive Module Pattern is lauded as a best practice, and I think it is, but we really have to be diligent in how we use it. One issue that is always under debate is how much code do you make private? In my opinion, very very little code should ever be private for the simple fact that it makes it harder to test. You can prepend method names with an underscore if you don't intend it to be accessed externally. If it does anything of any real value, it should be tested. If you really want to make it private, pull your private code into another module (with public methods) so that it can be tested there without messing with your primary module's interface. Below is an example of the Module Pattern used poorly. We can fix this in two ways. One is to simply expose all of those methods. Another way is to pull this into two modules. One acts as the interface for the other, but both have accessible methods for testing. In general, I use the second method here. h2. The Problem with Auto-init I have seen several modules that define a module then call its init method immediately. This pattern has one major advantage and two major drawbacks. The advantage is that it's self-contained in one file. All of the logic required to create and use the module can be triggered by including one script. This is nice, but I hope that you will see how the drawbacks outweight this benefit. The drawbacks are: (1) In order to test this, init has already been called before your spec can run. In many cases, this can throw an error. In others, it may just behave differently the second time it's called. (2) The module iteself becomes less resusable. You can't just include this module and decide when to call the initialize method. Further, you can't include the module and only call certain methods on it (and not initiailze). Therefore, I believe that auto-init script files are a bad pattern. Below is a simple example of this pattern at work: Note that Backbone.js views that call render in their initialize methods are a form of this Auto-init pattern. h2. Using Console log/warn/error The browser's console lists messages that come from errors, warnings and log messages triggered by the code on the page. If a script triggers one of these things while loading (not during a spec run), it will log the error to the console, but the suite will pass. This is a huge problem. An error in the console implies: # a poor understanding of the code under test # untested code # actual bugs It also makes it a lot harder to debug your own specs if the log is already full of dozens of errors and/or messages. h2. Spec Suite Health Inspired by the issues discovered above, I constructed a series of Suite Health Specs. They are a group of specs that run at the very end of your entire suite to make sure that none of your tests polluted areas of global scope. This helper sets up some collections to keep track of a few method calls. We log these calls for later because you will often use console.log (and others) during debugging to check execution order and values. Then, we set up this set of specs to run after all of our other specs. These will verify our collections as well as areas of global state. In order to get this setup properly, we need to place them in your jasmine.yml file, like so: Now, we have a spec suite that will fail when a spec pollutes global scope! Note that the above does not have a spec that checks for global javascript scope pollution. There are ways to accomplish this, but I'll leave that as an excercise for the reader. h2. Summary These practices are all things we have probably heard before. It may have been in school or in practice. We simply need to recognize that code is code: these problems exist in any language and we should be diligent in understanding and controlling them.