Jasmine
Jasmine specs are just JavaScript. Jasmine doesn’t change the way your code loads or runs. — Jasmine home page
Of the many JavaScript testing frameworks, I’ve settled on Jasmine
because it involves simply editing an html file — the example included
in the download
zip file is called SpecRunner.html — and linking it to
something akin to the included example
spec/PlayerSpec.js.
I like to write a separate test page for each module, so I copy
SpecRunner.html to test/modulename.html and
edit it into something like:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Card Module Tests</title>
<link rel="shortcut icon" type="image/png" href="/jasmine/lib/jasmine-6.3.0/jasmine_favicon.png">
<link rel="stylesheet" href="/jasmine/lib/jasmine-6.3.0/jasmine.css">
<script src="/jasmine/lib/jasmine-6.3.0/jasmine.js"></script>
<script src="/jasmine/lib/jasmine-6.3.0/jasmine-html.js"></script>
<script src="/jasmine/lib/jasmine-6.3.0/boot0.js"></script>
<!-- optional: include a file here that configures the Jasmine env -->
<script src="/jasmine/lib/jasmine-6.3.0/boot1.js"></script>
<script type="module" src="cardTests.js"></script>
</head>
<body>
</body>
</html>Since modulenameTests.js is type="module"
it can import whatever module is being tested.
While I like to have a separate html file for each module while I
develop it, any number of test files can be included, so I also have an
index.html file combining all the tests. Jasmine shuffles
the order it runs these files along with the tests they contain as
explained in the Why are tests run in a random
order? section.
Due to a bit of pedantry, I prefer
test/modulenameTests.js to
spec/modulenameSpec.js as explained in test vs spec. Spec is the commonly used jargon
in behaviour driven development, and testing has
unfortunately been overcomplicated by various tribes using different
terms.
I have test/modulenameTests.js open in one tab of my
editor and modules/modulename.js next to it, first writing
a list of examples in the tests file.
/**
* @file Tests for card module
*/
import card from "../modules/card.js";
describe("card functions", function() {
it ('The rank of "./cards/ace_of_spades.svg" should be ace', function() {
expect(card.rank("./cards/ace_of_spades.svg")).toBe("ace");
});
it ('The suit of "./cards/ace_of_spades.svg" should be spades', function() {
expect(card.suit("./cards/ace_of_spades.svg")).toBe("spades");
});
});Example-driven development
The first thing I tend to look for in documentation are examples of how to use a given package. A reason I like test-driven development is it leaves examples in its wake which make for handy reference later. Writing examples before diving into the code also helps shape the end result into something nicely designed rather than hacked together hastily.
A really nice introduction to this style of development is taught by Gregor Kiczales in his MooC based on the free online textbook How To Design Programs which encourages novice programers to follow this six step recipe:
- From Problem Analysis to Data Definitions
- Signature, Purpose Statement, Header
- Functional Examples
- Function Template
- Function Definition
- Testing
Step 1 is part of documentation which I’ve given an example of using JSDoc in Documenting data definitions.
Step 3, writing examples of what you want your function to
do rather than bothering with how yet, can be done with Jasmine
by initially using xit test blocks which are just reminders
to develop those parts later.
xit('Rule5aj Discard cards of same rank or suit, or discard joker', function() {
});On the browser page these are shown as yellow:
There’s a Wikipedia page on specification by example, a term coined by Martin Fowler.
Learn by testing
A big selling point of writing tests which often isn’t stressed enough is that it’s a great way of learning a programing language, playing around to see how things work.
The Wikipedia page on test-driven development provides Kent Beck’s six-step “coding cycle” which is similar to the HTDP recipe:
- List scenarios for the new feature
- Write a test for an item on the list
- Run all tests. The new test should fail – for expected reasons
- Write the simplest code that passes the new test
- All tests should now pass
- Refactor as needed while ensuring all tests continue to pass
The advice is to initially just create stubs as you develop, Fake it till you make it, and then flesh these out gradually.
Testing as you go helps gamify the process, going from yellow to red to green.
Why are tests run in a random order?
I learnt this the hard way writing an online exam on SQL where I assumed the questions were to be answered in order with the database in the state left by the last question.
It turns out that in both unit testing and online exams, the same initial state is assumed each time.
Shuffling the order of the tests ensures each test follows the best practices where each test does a setup of a starting state.
Test isolation
Whereas it’s ok to import many modules into a module, indicating those modules contain general purpose functions handy to many clients, module test files should only import the module being tested so there’s a single point of truth (aka spot) to debug.
The starting state for a test should be hardcoded. The Wikipedia entry for test double provides a long list of jargon terms for this, and Jasmine uses spy which since I do class free or classless programing, I never use.
By using object literals as the interface to a module and hardcoding these as required for each test, I find it fairly easy to write tests without needing to understand the supposed differences between a stub, a mock, a spy, a fake, a dummy…
Object Equality
The trouble here is that substitution is based ultimately on the notion that the symbols in our language are essentially names for values. But as soon as we introduce set! and the idea that the value of a variable can change, a variable can no longer be simply a name. Now a variable somehow refers to a place where a value can be stored, and the value stored at this place can change. — Structure and Interpretation of Computer Programs
Programers learning C or its descendents have to grasp the unary
operators * and &. The asterisk is used to
declare a variable contains a place in memory (aka pointer),
and prefixing that variable with an ampersand returns what is stored in
that place. Without the ampersand, it returns the address.
Eighties scripting languages, such as JavaScript and Python, made
* and & unfashionable. Since C programers
typically only used them for abstract
data types, such as what JavaScript calls objects, Python calls
dictionaries, Awk calls associative arrays… these languages sneakily
created variables that behave completely differently to strings and
numbers. Instead of helping, this “simplified” syntax hides piles of dog
poo that every novice programer steps in at some point.
I’m going to use some code written in a JavaScript testing suit Jasmine to illustrate a common mistake:
describe("object equality", function() {
const a = {"x": 1, "y": 2};
const b = {"x": 1, "y": 2};
it("a === b", function() {
expect(a === b).toBe(true);
});
});
The easiest way to test if two object literals contain the same values is to use JSON.stringify().
describe("object equality", function() {
const a = {"x": 1, "y": 2};
const b = {"x": 1, "y": 2};
it("a === b", function() {
expect(JSON.stringify(a) === JSON.stringify(b)).toBe(true);
});
});
This isn’t perfect since the order of keys shouldn’t matter, so if
b were {"y": 2, "x": 1} it should still equal
a. Equality get even more complex for nested abstract data
types, commonly needed for game trees.
describe("object equality", function() {
const a = {"x": 1, "y": 2};
const b = {"y": 2, "x": 1};
it("expect(a).toEqual(b)", function() {
expect(a).toEqual(b);
});
});
My most recent stepping into this “place not value” dog poo was done
writing my initialisation code for an object literal without
structuredClone like so:
game.new = function (state) {
Object.keys(START).forEach(function(k) {
state[k] = START[k];
});
};The first time the code is run, state.Attack1 is set to
the memory address created when START.Attack1 is first
loaded from start.json. The problem emerges after when
game.new(state) is called a second or third time. We want
the value of state.Attack1 to be set to an empty array, but
the above code sets it to the previous address alocated to
START.Attack1, ie no change. Whatever was put in the array
at that address during the previous game still resides there. A fresh
copy of START has to be used with new addresses holding empty
arrays.
game.new = function (state) {
const init = structuredClone(START);
Object.keys(init).forEach(function(k) {
state[k] = init[k];
});
};Behaviour Driven Development Jargon
Design by Contract
Client must ensure precondition.
Supplier may assume precondition.
Arrange Act Assert
Why test, not spec
Programmers who advocate writing tests before writing code often believe those tests can serve as a specification. Writing tests does force us to think, and anything that gets us to think before coding is helpful. However, writing tests in code does not get us thinking above the code level. We can write a specification as a list of high-level descriptions of tests the program should pass—essentially a list of properties the program should satisfy. But that is usually not a good way to write a specification, because it is very difficult to deduce from it what the program should or should not do in every situation. — Leslie Lamport, Who Builds a House Without Drawing Blueprints?
I don’t like using “spec” as a synonym for “test”:
Tests are not a replacement for specification: they follow from these specifications. It is indeed remarkable to see how the presence of contracts can drive the testing process. If a slogan is needed and two can do, I will venture “Contract-driven testing” and “Test-obsessed development”. That works very well – you should test all the time, with the intent of finding bugs – but it’s not a reason to drop specification and design. Specification and design are what propels both the testing process and the test cases themselves. — Bertrand Meyer, Test or spec? Test and spec? Test from spec!