All About HTML5 Game Development, from Publishing to Monetization

HTML5 Game unitary testing with Jest in ExcaliburJS engine

HTML5 Game unitary testing with Jest in ExcaliburJS engine

This is an open-source project on game development with a focus on the HTML5 Excalibur game engine. Check out this feature implementation on this commit in our Github repository!

You can read here the list of features we are going to develop for our Excalibur game, such as an Ionic React UI interface. If you're interested in game development, follow us on Twitter and Instagram to be caught up on our latest news!

Table of Contents

In this tutorial

In this tutorial we set up jest for writing unitary tests.

In the last tutorial we added an animated character to our game. We've done a lot of work and we've written a lot of new code. Now it's time to run some tests on it!

Jest setup

Dependencies

To set up Jest to work with Webpack, we'll follow what's written in Jest documentation.

Let's begin by installing the Jest and babel dependencies for working with Typescript. Open the terminal and run


npm install --save-dev jest
npm install --save-dev babel-jest @babel/core @babel/preset-env @babel/preset-typescript
                    

Since we are working in a browser environment, we also need to install the jsdom and the canvas jest packages:


npm install --save-dev jest-environment-jsdom
npm i --save-dev jest-canvas-mock
                    

Configuration files

Now that we've installed all the required dependencies, we can proceed by setting up the configuration files. After having had a look at Jest documentation for setting up Jest and Webpack, we create a jest.config.ts file under the project root folder sword-adventure.


/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFiles: [
    "jest-canvas-mock"
  ],
  setupFilesAfterEnv: ['<rootDir>/test.setup.ts'],
  moduleNameMapper: {
    "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/fileMock.js",
    "\\.(css|less)$": "<rootDir>/test/fileMock.js",
  },
  transform: {"^.+\\.(js|jsx|mjs)$": "<rootDir>/node_modules/babel-jest"},
  transformIgnorePatterns: ["/node_modules"],
  resolver: undefined
};
    

Let's have a look at this configuration file.

  • The test environment is jsdom, since Excalibur runs into a browser. If we were developing for example a server with node, the test environment would be another one.
  • We need jest-canvas-mock to mock the canvas on which Excalibur relies on.
  • The test.setup.ts is a file placed at the same level of jest.config.ts, containing a single import:
    
    import 'reflect-metadata';
                                
    We need this import for the typedi library to work. In production code, we've included this import in index.ts file.
  • The module name mapper contains a sort of "redirection" so that real assets file won't be imported when testing. In their place, there is a mock file which exports a simple string: fileMock.js is as simple as
    
    module.exports = 'test-file-stub';
                            
  • The last two options, transform and transformIgnorePatterns, are a way to transform code that Jest is not able to run by itself. You can read more about this topic here.

The last configuration file we're missing is babel.config.js, which by following the documentation is as simple as


module.exports = {
    presets: [
        ['@babel/preset-env', {targets: {node: 'current'}}],
        '@babel/preset-typescript',
    ],
};
                    

Set up package.json scripts

Now that all the dependencies are installed and that all the configuration files are ready, we can add a few commands in our package.json scripts:


...
"scripts": {
    "serve": "webpack serve --config webpack.development.js",
    "jest-test": "jest",
    "build:dev": "webpack --config webpack.development.js",
    "build:prod": "npm run jest-test && webpack --config webpack.production.js",
  }
...
                    

As you can see, now before building the source code in production mode, we run tests.

Unitary tests

We're ready to start writing the tests. The classes that needs them are ExcaliburActor, PlayScene and GraphicService.

ExcaliburActor

We should test that after calling the onInitialize() method, the GraphicService has been called to register actor graphics:


describe('ExcaliburActor', () => {

    let graphicService: GraphicService;

    beforeEach(() => {
        graphicService = spy(Container.get(GraphicService));
    });

    afterEach(() => {
        reset(graphicService);
    });

    it('should initialize actor', () => {
        const args: ActorArgs = {} as ActorArgs;
        const actor: ExcaliburActorTest = new ExcaliburActorTest(args, {tag: 'test_tag', collisionGroupKey: 'collisiong_group'});
        const engine: Engine = mock(Engine);

        actor.onInitialize(engine);

        verify(graphicService.registerActorGraphics('player', actor)).once();
        verify(graphicService.registerActorAnimations('player', actor)).once();
    });

});

class ExcaliburActorTest extends ExcaliburActor {

    public COLLISION_GROUP_NAME: string = 'test';
    public type: ActorType = 'player';

}

                    

In this test we're using ts-mockito mocking library. We could use only Jest, but in our opinion ts-mockito is easier, so we'll use it.

Back to the ExcaliburActor test, it's pretty simple and straightforward. We just need to spy on the GraphicService to check with a couple of verify that it has been called after actor initialization.

PlayScene

The PlayScene is quite simple, as the ExcaliburActor one. Also in this case we need to check that after initializing the scene, a player actor is added to it. The easiest way to test it is by performing a little refactor on the PlayScene. If the actor was created using a ActorFactory, then we would check that the ActorFactor was called once. The ActorFactory can be done as:


@Service()
export class ActorFactory {

    public createPlayer(actorArgs: ActorArgs): Player {
        const {TAGS, COLLISION_GROUPS} = ActorTags;
        const actorConfig: ActorConfig = {tag: TAGS.player, collisionGroupKey: COLLISION_GROUPS.player};
        return new Player(actorArgs, actorConfig);
    }

}
                    

Accordingly, the PlayScene changes as:


export class PlayScene extends Scene {

    private actorFactory: ActorFactory = Container.get(ActorFactory);

    public onInitialize(): void {
        ...
        const args: ActorArgs = {
            pos: vec(width / 2, height / 2),
            collisionType: CollisionType.Active,
            collider: Shape.Box(100, 100),
        };
        const player: Player = this.actorFactory.createPlayer(args);
        ...
    }

}
                    

With the code being refactored as above, the PlayScene class test is quite easy and resembles the one of the ExcaliburActor, where we spy on a service and verify that the service has been called:


describe('PlayScene', () => {

    let actorFactory: ActorFactory;

    beforeEach(() => {
        actorFactory = spy(Container.get(ActorFactory));
    });

    afterEach(() => {
        reset(actorFactory);
    });

    it('should initialize the scene with a player actor', () => {
        const scene: PlayScene = new PlayScene();

        scene.onInitialize();

        expect(scene.actors.length).toBe(1);
        verify(actorFactory.createPlayer(anything())).once();
    });

});

                    

By creating the ActorFactory class we've gained another class to test, but luckily enough its test is as easy as the others:


describe('ActorFactory', () => {

    let actorFactory: ActorFactory;

    beforeEach(() => {
        actorFactory = new ActorFactory();
    });

    it('should create player actor', () => {
        const actorArgs: ActorArgs = {
            pos: vec(50, 50),
            collisionType: CollisionType.Active,
            collider: Shape.Box(100, 100),
        };

        const player: Player = actorFactory.createPlayer(actorArgs);

        expect(player.tags.includes(ActorTags.TAGS.player)).toBe(true);
    });

});
                    

GraphicService

Last but not least we have to test the GraphicService, which is perhaps the most important class to test until now since if it doesn't work, nothing will be displayed properly in the game.



describe('GraphicService', () => {

    let graphicService: GraphicService;
    let allGraphics: typeof AllGraphics;

    beforeEach(() => {
        graphicService = new GraphicService();
        allGraphics = spy(AllGraphics);
    });

    it('should register actor graphics', () => {
        const actor: Actor = new Actor();
        const expected: Graphic = mock(Graphic);
        const actorGraphic: ActorGraphic = {player: [{graphic: expected, name: 'hurt'}]};
        when(allGraphics.textures).thenReturn(actorGraphic)

        graphicService.registerActorGraphics('player', actor);

        const graphicsComponent: GraphicsComponent = actor.graphics;
        const actual: Graphic = graphicsComponent.getGraphic('hurt');
        expect(actual).toEqual(expected);
    });

    it('should register actor animations', () => {
        const actor: Actor = new Actor();
        const expected: Animation = mock(Animation);
        const actorAnimations: ActorAnimations = {player: [{animation: expected, name: 'run'}]};
        when(allGraphics.animations).thenReturn(actorAnimations)

        graphicService.registerActorAnimations('player', actor);

        const graphicsComponent: GraphicsComponent = actor.graphics;
        expect(Array.from(Object.keys(graphicsComponent.graphics)).length).toBe(1);
        const actual: Graphic = graphicsComponent.getGraphic('run');
        expect(actual).toEqual(expected);
    });

});

                    

In this test we are mocking the AllGraphics module. This is not strictly necessary since at this stage we could easily use the real one.

The two tests consist in checking that the actor has the graphics as configured in the AllGraphics mocked module.

Conclusions

In this tutorial we've set up a jest configuration for a Typescript and Webpack npm project, and we wrote some unitary tests for our classes. At this stage, we have a very simple Excalibur game that contains a simple scene and an animated character. The next step is to control the actor using a keyboard or a virtual joystick: check out our next tutorial!

Did you like this article and wish there were more? Donate on Paypal so that I can write more!

Related Articles