simonw / datasette

An open source multi-tool for exploring and publishing data
https://datasette.io
Apache License 2.0
9.32k stars 666 forks source link

Mechanism for executing JavaScript unit tests #1165

Open simonw opened 3 years ago

simonw commented 3 years ago

I'm going to need to add JavaScript unit tests for this new plugin system.

Originally posted by @simonw in https://github.com/simonw/datasette/issues/983#issuecomment-752757289

simonw commented 3 years ago

https://jestjs.io/ looks worth trying here.

simonw commented 3 years ago

https://www.valentinog.com/blog/jest/ was useful.

I created a static/__tests__ folder and added this file as plugins.spec.js:

const datasette = require("../plugins.js");

describe("Datasette Plugins", () => {
  test("it should have datasette.plugins", () => {
    expect(!!datasette.plugins).toEqual(true);
  });
  test("registering a plugin should work", () => {
    datasette.plugins.register("numbers", (a, b) => a + b, ["a", "b"]);
    var result = datasette.plugins.call("numbers", { a: 1, b: 2 });
    expect(result).toEqual([3]);
    datasette.plugins.register("numbers", (a, b) => a * b, ["a", "b"]);
    var result2 = datasette.plugins.call("numbers", { a: 1, b: 2 });
    expect(result2).toEqual([3, 2]);
  });
});

In static/plugins.js I put this:

var datasette = datasette || {};
datasette.plugins = (() => {
    var registry = {};
    return {
        register: (hook, fn, parameters) => {
            if (!registry[hook]) {
                registry[hook] = [];
            }
            registry[hook].push([fn, parameters]);
        },
        call: (hook, args) => {
            args = args || {};
            var results = [];
            (registry[hook] || []).forEach(([fn, parameters]) => {
                /* Call with the correct arguments */
                var result = fn.apply(fn, parameters.map(parameter => args[parameter]));
                if (result !== undefined) {
                    results.push(result);
                }
            });
            return results;
        }
    };
})();

module.exports = datasette;

Note the module.exports line at the end.

Then inside static/ I ran the following command:

% npx jest -c '{}'
 PASS  __tests__/plugins.spec.js
  Datasette Plugins
    ✓ it should have datasette.plugins (3 ms)
    ✓ registering a plugin should work (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.163 s
Ran all test suites.

The -c {} was necessary because I didn't have a Jest configuration or a package.json.

simonw commented 3 years ago

Turned that into a TIL: https://til.simonwillison.net/javascript/jest-without-package-json

simonw commented 3 years ago

I don't know if Jest on the command-line is the right tool for this. It works for the plugins.js script but I'm increasingly going to want to start adding tests for browser JavaScript features - like the https://github.com/simonw/datasette/blob/0.53/datasette/static/table.js script - which will need to run in a browser.

So maybe I should just find a browser testing solution and figure out how to run that under CI in GitHub Actions. Maybe https://www.cypress.io/ ?

simonw commented 3 years ago

Jest works with Puppeteer: https://jestjs.io/docs/en/puppeteer

simonw commented 3 years ago

I got Cypress working! I added the datasette.plugins code to the table template and ran a test called plugins.spec.js using the following:

context('datasette.plugins API', () => {
    beforeEach(() => {
      cy.visit('/fixtures/compound_three_primary_keys')
    });
    it('should exist', () => {
        let datasette;
        cy.window().then(win => {
            datasette = win.datasette;
        }).then(() => {
            expect(datasette).to.exist;
            expect(datasette.plugins).to.exist;
        });
    });
    it('should register and execute plugins', () => {
        let datasette;
        cy.window().then(win => {
            datasette = win.datasette;
        }).then(() => {
            expect(datasette.plugins.call('numbers')).to.deep.equal([]);
            // Register a plugin
            datasette.plugins.register("numbers", (a, b) => a + b, ['a', 'b']);
            var result = datasette.plugins.call("numbers", {a: 1, b: 2});
            expect(result).to.deep.equal([3]);
            // Second plugin
            datasette.plugins.register("numbers", (a, b) => a * b, ['a', 'b']);
            var result2 = datasette.plugins.call("numbers", {a: 1, b: 2});
            expect(result2).to.deep.equal([3, 2]);
        });
    });
});
simonw commented 3 years ago

Important to absorb the slightly bizarre assertion syntax from Chai - docs here https://www.chaijs.com/api/bdd/

simonw commented 3 years ago

https://github.com/PostHog/posthog/tree/master/cypress/integration has some useful examples, linked from this article: https://posthog.com/blog/cypress-end-to-end-tests

Also useful: their workflow https://github.com/PostHog/posthog/blob/master/.github/workflows/e2e.yml

dracos commented 3 years ago

Sorry to go on about it, but it's my only example ;) And thought it might be of interest/use. Here is FixMyStreet's Cypress workflow https://github.com/mysociety/fixmystreet/blob/master/.github/workflows/cypress.yml with the master script that sets up server etc at https://github.com/mysociety/fixmystreet/blob/master/bin/browser-tests (that has features such as working inside/outside Vagrant, and can do JS code coverage) and then the tests are at https://github.com/mysociety/fixmystreet/tree/master/.cypress/cypress/integration