-
Notifications
You must be signed in to change notification settings - Fork 34
JS Unit Testing Gotchas
This wiki will temporarily keep track of the weird nonsense that we've discovered individually while working out javascript unit testing.
Description of problem.
Description of solution.
Where an example of the solution can be found.
Example:
A witch turned me into a newt.
I got better.
Camelot.
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.
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.
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
.
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.
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:
- Ensure you are using
$timeout
in the controller and notsetTimeout
- Inject
$timeout
into your test - Run the function or action that invokes a timeout
- Call
$timeout.flush()
and$timeout.verifyNoPendingTasks()
- 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 setTimeout
s to $timeout
instead.
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.
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.
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.
https://clu.cdl.ucf.edu/materia/hangman, self-testing branch. See specifically tests/test.js, beforeEach(function(){});
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.
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
https://clu.cdl.ucf.edu/materia/Dodgeball, angular-rewrite branch. See specifically tests for AI-driven letter choice.
An event is called that changes elements on the screen. You need to make sure the elements were changed appropriately.
In your test file:
-
Create the element(s) -
var element = '<element id="unique"></element>
-
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 -
Emit the event that needs to be tested -
$scope.$emit('change-height')
-
Expect the element to have the updated changes-
expect(document.getElementById('unique').height).toEqual(500);
https://clu.cdl.ucf.edu/materia/radar-grapher/blob/self-testing/tests/test.js#L44