Skip to content

JS Unit Testing Gotchas

Ryan Eppers edited this page Oct 26, 2016 · 14 revisions

Purpose

This wiki will temporarily keep track of the weird nonsense that we've discovered individually while working out javascript unit testing.

Format

Brief Description of Problem

Problem

Description of problem.

Solution

Description of solution.

Location of Example

Where an example of the solution can be found.

Example:

Prove She's a Witch

Problem

A witch turned me into a newt.

Solution

I got better.

Location of Example

Camelot.

List

Multiple Controllers Per Module

Problem

In a lot of our widgets we use a single module, for example a single module widgetName and multiple controllers playerCtrl and creatorCtrl. However in many cases we define the module immediately before defining the controllers, or in other words we define the module both in the player controller file and in the creator controller file. Normally this is fine, as only one of these files is loaded at any given time. However in the context of unit testing our code, both files are used simultaneously.

This is problematic due to the module being defined once, then a controller being defined, then the module being defined again, then another controller being defined. This way, the first controller is lost after the module is defined the second time.

Solution

Splitting Angular components into their own files, such as a main.js file that contains the module's initial definition rather than defining the module in both the player and creator controller files. This way we can define the module once and define each controller independently. This requires a change to the script inclusion blocks in the player and creator .html files, but it allows us to test both the creator and the player together without any issues.

Location of Example

https://clu.cdl.ucf.edu/materia/hello-world-widget, self-testing branch. See specifically player.html, src/modules/main.coffee, src/controllers/player.coffee, and tests/test.js.

Testing timeouts

Problem

Some controllers use $timeout or setTimeout to delay an action. When testing, everything should be synchronous and there shouldn't be any delays. Jasmine ignores any timeouts and just moves on.

Solution

Jasmine supports using $timeout into your test to immediately run whatever needs to be run to continue the testing. This solution only works with $timeout and not setTimeout. To do this:

  1. Ensure you are using $timeout in the controller and not setTimeout
  2. Inject $timeout into your test
  3. Run the function or action that invokes a timeout
  4. Call $timeout.flush() and $timeout.verifyNoPendingTasks()
  5. Check for what you were expecting to happen after the timeout runs.

For setTimeout you are supposed to use jasmine.clock().tick() but I have not gotten this to work and opted to change the setTimeouts to $timeout instead.

Location of Example

https://clu.cdl.ucf.edu/materia/this-or-that/blob/self-testing/tests/test.coffee#L91 and https://clu.cdl.ucf.edu/materia/this-or-that/blob/self-testing/src/player.coffee#L88. This or That widget's self-testing branch. See specifically tests/test.coffee for injecting $timeout into tests and src/player.coffee for the corresponding code that is being tested.

Testing Functions that Rely on the DOM

Problem

Some widgets (even angular widgets), require specific elements from the DOM to run correctly. This becomes a problem when testing with Jasmine, because Jasmine is DOM independent. Therefore, tests will fail because the DOM elements the code is trying to load does not exist in the test. Another problem with the inclusion of DOM elements is keeping each test's independence.

Solution

Best Solution: Remove the dependence of DOM elements if possible (especially in angular widgets).

OK Solution: Create the DOM elements needed before each test is ran, and delete the elements after each test. Creating and removing the DOM elements will provide each test with a fresh set of elements. This can all be done in javascript in the beforeEach function built into Jasmine.

Location of Example

https://clu.cdl.ucf.edu/materia/hangman, self-testing branch. See specifically tests/test.js, beforeEach(function(){});

Controlling Math.random()

Problem

Some of our widgets rely on Javascript's Math.random() for some purpose or another. This is difficult to test since, obviously, we can't accurately predict random output. Even in cases where we're just trying to make sure input doesn't match output, there's no guarantee the test will pass.

Solution

With Jasmine, it's possible to intercept calls to just about any function. We can use this to our advantage, catching any attempt by the widget to call Math.random and instead return our own value. We can even control that output easily by having a custom function run in the place of the original. Example:

var desiredOutput = 0;
spyOn(Math, 'random').and.callFake(function (){
    return desiredOutput;
});
console.log(Math.random()); // 0
desiredOutput = 1;
console.log(Math.random()); // 1

Location of Example

https://clu.cdl.ucf.edu/materia/Dodgeball, angular-rewrite branch. See specifically tests for AI-driven letter choice.

Elements that are Changed on Event

Problem

An event is called that changes elements on the screen. You need to make sure the elements were changed appropriately.

Solution

In your test file:

  1. Create the element(s) - var element = '<element id="unique"></element>

  2. Insert the element onto the DOM - document.body.insertAdjacentHTML( 'afterbegin', element); Make sure to insert the element within the correct location. The example above is inserted directly after the body tag. This will make sure we are able to find the element later

  3. Emit the event that needs to be tested - $scope.$emit('change-height')

  4. Expect the element to have the updated changes- expect(document.getElementById('unique').height).toEqual(500);

Location of Example

https://clu.cdl.ucf.edu/materia/radar-grapher/blob/self-testing/tests/test.js#L44