Testing HTML and JavaScript solutions

We use Jasmine as a testing framework for both HTML and JavaScript coding exercises.

Hello, World!

Before we go into details, let's see how you can use Jasmine to test the most basic HTML task: writing a paragraph that contains "Hello, World!".

This is what a student might write:

<html>
    <body>
        <p>Hello, World!</p>
    </body>
</html>

And this is an example of what you could write to test or evaluate the student's code:

describe('paragraph', function() {
    var p = document.getElementsByTagName('p')[0];

    it('should exist', function() {
        expect(p).toBeDefined();
    });

    it('should show "Hello, World!" on the screen', function() {
        var content = p.textContent.trim();
        expect(content).toEqual('Hello, World!');
    });
});

Let's examine the above evaluation code. Besides the 3 functions that Jasmine provides, describe , it , and expect , the rest is just JavaScript code that has access to the context of the student's HTML document.

As you probably guessed, you can test JavaScript code that students write just as well as the HTML element above.

describe

describe defines the name of the subject that you want to test. Its value can be any string that is relevant to your exercise. You can have multiple describe calls in your evaluation if you need to test different subjects.

it

Inside describe , you can define as many tests as you want that are related to that same subject, by calling the it function. The string argument that it receives describes how the subject is expected to behave.

Inside it and describe you have access to the entire context of the student HTML document. You can:

  • access document , or window , just like the student can.
  • get references to elements from the student's code and interact with them (for example: you can click on a button on the page).
  • call JavaScript functions that the student has defined.

Tip: Jasmine will connect the 2 strings passed to describe and it to give a meaningful description of the test. In the above example, if any of the tests fail, this is what the student will see:

  • paragraph should exist
  • paragraph should show "Hello, World!" on the screen

expect

Finally, the expect function is what actually decides whether a test fails or succeeds. Once you have a reference to a piece of the DOM (or other value) generated by student's code, you can assert whether it behaves as it should by calling:

expect(valueFromStudentCode).toEqual(expectedValue);

toEqual() is called a Jasmine "matcher" and there are many more that you can use, like:

  • toBeDefined()
  • toBeNull()
  • toBeLessThan()

You can read about the rest in Jasmine's official documentation.

A matcher can be prepended with not if you want a negative assertion.

expect(valueFromStudentCode).not.toEqual(expectedValue);

If any of the expect calls fail inside a test, that test will be marked as failed by Jasmine and reported back to the student. If you want to give the student more granular feedback about a failing assertion, in your own words, you can pass an additional string argument to the matcher.

expect(valueFromStudentCode)
    .toEqual(expectedValue, 'Your code does not work because this and that.');

since

Another more robust alternative to give granular feedback to the user about a failing assertion is since() , which is provided by the jasmine2-custom-message library.

    since('Your code does not work because this and that.')
        .expect(valueFromStudentCode)
        .toEqual(expectedValue);

Let's see some more examples of things you can test:

Example 1: CSS Styling

Let's say you want to verify that a paragraph has proper CSS styling, say color "red". Students can implement the code in multiple ways, either by inlining the style:

<p style="color: red;">Some text</p>

or using a css selector in an external file:

p {
    color: red;
}

No matter how they implement it, you can test it by using the window.getComputedStyle function:

describe('the paragraph', function() {
    it('should be red', function() {
        var p = document.getElementsByTagName('p')[0];
        var style = window.getComputedStyle(p);
        expect(style.color).toEqual('rgb(255, 0, 0)');
    });
});

Example 2: User interactions

How do you test whether your students handle events, like the click of a button? Let's say that a button click must change the color of a paragraph to "blue". This is what a solution could look like:

<p style="color: red;">Some text</p>
<button onclick="javascript:changeColor();">Change color</button>
function changeColor() {
    var p = document.getElementsByTagName('p')[0];
    p.style.color = 'blue';
}

And this is how you would test it:

describe('"Change color" button', function() {
    var p = document.getElementsByTagName('p')[0];
    var button = document.getElementsByTagName('button')[0];

    it('should exist', function() {
        expect(button).toBeDefined();
    });

    it('should change the paragraph color to red', function() {
        button.click();
        var style = window.getComputedStyle(p);
        expect(style.color).toEqual('rgb(0, 0, 255)');
    });
});

What else is possible?

Jasmine's documentation presents more advanced functionality that you can use, like:

  • asynchronous testing via the done callback
  • mocks: spyOn
  • beforeEach , afterEach

You can access everything that was printed via console.log() in consoleOutput.loggedMessages, which is an array of strings: each string represents a call to console.log from the user code.

You can have your students include any external JavaScript from CDNs, as long as they are served over https. You can include:

  • jQuery
  • Underscore
  • Angular
  • React
  • anything else

For HTML coding exercises you can define only 1 HTML file, and as many JavaScript and CSS files as you want.

For JavaScript coding exercises you can define as many JavaScript files as you want.

Here are a few more JavaScript examples of things you can test:

Example 3: Multiple files

Here’s a simple example with 2 files, one that implements a function foo() that is expected to return true , and another one with a function bar() that is expected to return false .

foo.js

function foo() {
    return true;
}

bar.js

function bar() {
    return false;
}

The evaluation file will be very similar to the ones we write for HTML:

describe('foo()', function() {
    it('should return true', function() {
        expect(foo()).toBe(true);
    });
});

describe('bar()', function() {
    it('should return false', function() {
        expect(bar()).toBe(false);
    });
});

Example 4: Merge & sort arrays

Implement a function that accepts 2 arrays of numbers as arguments and returns a single array with all the numbers from both arrays, sorted in ascending order. The function should throw an exception if the arguments that are passed are not arrays of numbers.

The above exercise can be tested like this:

describe('mergeAndSort()', function() {
    it('should throw if the arguments aren\'t arrays', function() {
        expect(mergeAndSort()).toThrow();
        expect(mergeAndSort(1)).toThrow();
        expect(mergeAndSort(1, "a")).toThrow();
    });

    it('should throw if the arrays do not contain only numbers', function() {
        expect(mergeAndSort([1, 2], ['a', 2])).toThrow();
    });

    it('should merge arrays', function() {
        expect(mergeAndSort([1, 2], [2, 3])).toEqual([1, 2, 2, 3]);
    });

    it('should sort the result', function() {
        expect(mergeAndSort([2, 1], [3, 2])).toEqual([1, 2, 2, 3]);
    });
});

The more edge cases you can think of to test, the more helpful it will be for the students to help them write a correct solution.

Example 5: Spying on console.log()

This is a function that prints to standard output via console.log() :

function search() {
    console.log(2);
    console.log('abc', 3);
}

It can be tested by using spies like this:

it('prints to standard output', function() {
    var stdout = '';

    spyOn(console, 'log').and.callFake(function() {
        stdout += Array.prototype.slice.call(arguments).join(' ') + '\n';
    });

    search();

    expect(stdout).toEqual('2\nabc 3\n');
});

Technical specs

  • We use Jasmine version >= 2.4.1.
  • The browser that is used to evaluate student code is PhantomJS version >= 2.1.7.
  • All CSS files that you define will be included automatically (by us) at the end of the <head> tag.
  • All JavaScript files that you define will be included automatically (by us) at the end of the <body> tag.

Limitations

  • Like most older browsers, PhantomJS does not support ES6, so you cannot use ES6 in the HTML file.
  • We do support ES6 in the JavaScript files that you define in your coding exercise. We compile them with babel and add any necessary ES6 polyfills before serving them to PhantomJS.
  • We do not support ES6 modules in JavaScript files.
  • Students cannot define variables or functions that have the same name as the ones defined by Jasmine (describe , it , expect , fail and so on), or that are reserved (full list here).
  • Since we load all your JavaScript files in the context of an HTML page, this means that files will be loaded and executed in order, one after the other. You cannot call a function in file A if the function is defined in file B and file B is loaded after file A. Also, your evaluation file will always be the last one.

results matching ""

    No results matching ""