6.102
6.102 — Software Construction
Spring 2024

Project Testing

Automated Testing

Starting with Problem Set 0, you have been using the Didit system for automated building and testing: every time you push your commits to GitHub, the Didit build server pulls a copy of your code, attempts to compile it, and runs tests.

This kind of automation is very common in the world of professional software development, and is very useful for coordinating a team of developers. There is no ambiguity about whether the code compiles or not: if it doesn’t compile on the build server, it doesn’t compile. And there is no ambiguity about whether the tests pass: if they don’t pass on the build server, they don’t pass.

Restrictions

You may not use packages other than those included in the provided package.json.

Your tests must not use unreasonable amounts of space (e.g. large images) or time (e.g. slow concurrency tests).

Your code should use localhost ports 8700—8799 to communicate between servers and clients.

As always, do not attempt to run anything other than your project tests.

Excluding tests

You may have some tests that Didit cannot run. Don’t let Didit’s restrictions stop you from writing such tests.

Instead, any tests that mention “Didit” anywhere in their name will be excluded from running on Didit.

Unfortunately, these tests will not be listed at all on the build results page. Make sure you and your teammates are running the skipped tests manually!

How to test: canvas drawing

In the browser, HTMLCanvasElement objects refer to <canvas> HTML elements on the page.

To test drawing code that is designed primarily to draw on <canvas> elements in the browser, you may want to write Mocha tests that run in Node.js, as you did on Problem Set 3.

Here is one way to manage that. In the code that runs in the browser, define helper types:

// import only the types from 'canvas', not the implementations
import type { Canvas, CanvasRenderingContext2D as NodeCanvasRenderingContext2D } from 'canvas';

/**
 * Either: a CanvasRenderingContext2D in the web browser,
 *      or a NodeCanvasRenderingContext2D in Node (for testing)
 */
type WebOrNodeCanvasRenderingContext2D = CanvasRenderingContext2D | NodeCanvasRenderingContext2D;

/**
 * Either: a HTMLCanvasElement representing a `<canvas>` on the web page,
 *      or a Canvas representing a canvas in Node (for testing)
 */
type WebOrNodeCanvas = Omit<HTMLCanvasElement | Canvas, 'getContext'> & {
    getContext(contextId: '2d'): WebOrNodeCanvasRenderingContext2D | null;
};

The WebOrNodeCanvas type definition above works around an issue with getContext(..) that prevents us from using a simple union type.

If you have functions (or methods, constructors, etc.) that take in a HTMLCanvasElement to draw on, change them to take in a WebOrNodeCanvas instead. After that change, your code should continue to compile, bundle, and run in the web browser.

In your Mocha test code, import createCanvas(..) from the canvas package and use it to create Canvas instances. Then you can subsequently examine the image data of those Canvas objects as you did in PS3.

If your functions (or methods, constructors, etc.) take a rendering context instead, you can use the WebOrNodeCanvas­Rendering­Context2D union type above.

If you want to use drawImage, the definitions above are insufficient. You can add this bit of type wrangling:

import type { Image } from 'canvas'; // also import the Image type

declare global {
    // assert that the browser's drawImage works with Image
    interface CanvasDrawImage {
        drawImage(image: Image, dx: number, dy: number): void;
        // ... add other overloads if needed ...
    }
}

And use loadImage both in Node and in the browser:

import { loadImage } from 'canvas';

const context: WebOrNodeCanvasRenderingContext2D = ...;
const image = await loadImage('my-cool-image.png');
context.drawImage(image, 0, 0);

No browser dependencies

Keep in mind: code that runs in Node cannot use DOM APIs.

The following will not work:

test/DrawingTest.ts

import { createCanvas } from 'canvas';
import { draw } from '../src/StarbClient.js';  // oops!

describe('draw', function() {
    it('should work', function() {
        // make a canvas using createCanvas(..)
        // call draw(..)
    });
});

src/StarbClient.ts

// ... imports and type definitions ...

export function draw(canvas: WebOrNodeCanvas): void {
    // draws on the canvas, does not use `document`
}

function main(): void {
    // use `document.getElementById` to get the canvas
    // call draw(..)
}

main();

Importing StarbClient will run main() in the last line of the file. Since DrawingTest imports StarbClient, trying to run the tests immediately fails with:

ReferenceError: document is not defined

Here is a quick fix. Think about the best way to accomplish the same goal in your group’s design, which likely involves additional modules:

test/DrawingTest.ts

import { createCanvas } from 'canvas';
import { draw } from '../src/CoolGraphics.js'; // ok!  

describe('draw', function() {
    it('should work', function() {
        // make a canvas using createCanvas(..)
        // call draw(..)
    });
});

src/CoolGraphics.ts

// ... imports and type definitions ...

export function draw(canvas: WebOrNodeCanvas): void {
    // draws on the canvas, does not use `document`
}

src/StarbClient.ts

import { draw } from './CoolGraphics.js';

function main(): void {
    // use `document.getElementById` to get the canvas
    // call draw(..)
}

main();

How to test: web server

Testing a web server is a little different than the tests you’ve written so far in the course. In particular, you’ll likely need to use fetch() in order to simulate the client/server interaction.

See the Fetch API docs. Node fetch implements the same spec.

Here is an example of how a web server might be tested:

import assert from 'assert';
import { WebServer } from '../src/WebServer.js';
import { StatusCodes } from 'http-status-codes';

describe('server', function () {

    it('covers /hello with valid greeting', async function () {
        const server = new WebServer();
        await server.start();

        const url = `http://localhost:${server.port}/hello/w0rld`;

        // in this test, we will just assert correctness of the server's output
        const response = await fetch(url);
        assert.strictEqual(await response.text(), "Hello, w0rld!");

        server.stop();
    });

    it('covers /hello with invalid greeting', async function () {
        const server = new WebServer();
        await server.start();

        const url = `http://localhost:${server.port}/hello/world!`;

        // in this test, we will just assert correctness of the response code
        const response = await fetch(url);
        assert.strictEqual(response.status, StatusCodes.NOT_FOUND);

        server.stop();
    });

});