🏠Testing
By Robert Laing
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="cardSpec.js"></script>
</head>
<body>
</body>
</html>Since modulenameSpec.js is type="module" it
can import whatever module is being tested. 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.
So I tend to have test/modulenameSpec.js open in one tab
of my editor and modules/modulename.js in another,
initially only writing stubs in the module file and failing tests in the
spec.
import card from "../modules/card.js";
describe("card functions", function() {
it ('card.rank("./cards/ace_of_spades.svg")', function() {
expect(card.rank("./cards/ace_of_spades.svg")).toBe("ace");
});
it ('card.suit("./cards/ace_of_spades.svg")', function() {
expect(card.suit("./cards/ace_of_spades.svg")).toBe("spades");
});
});Behaviour Driven Development Jargon
Design by Contract
Client must ensure precondition.
Supplier may assume precondition.
Arrange Act Assert
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 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];
});
};