🏠 Testing

By Robert Laing

  1. Jasmine
  2. Behaviour-driven development
  3. Object Equality

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

Given When Then

Given When Then

Design by Contract

Client must ensure precondition.

Supplier may assume precondition.

Design by Contract

Arrange Act Assert

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);
  });

});
Object values are the same, but their addresses are not

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);
  });

});
Converting to strings makes the objects match

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);
  });

});
Jasmine’s toEqual matcher helps here

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];
  });
};