Search code examples
javascriptclassplaywrightplaywright-test

unit test class with playwright


I have a setup where I can do unit tests with playwright. The setup is (just the important files)

~/folder: ls
sum.js
tests/foo.spec.js

sum.js

function sum(a, b) {
  return a + b*2;
}

foo.spec.js

const { test, expect } = require('@playwright/test');

test.describe('simple suite test', () => {
    test('unit test sum.js function', async ({ page }) => {

        await page.addScriptTag({path: 'sum.js'});
        const data = await page.evaluate(() => window.sum(1,7));
        await expect(data).toBe(15); // 1+7*2=15
        
    });
 });

The test is executed through npx playwright test foo.spec.js --project chromium.

Now I wanted to call a class function, but I do not know how to do it? Somehow I have to create the object and call the function. But how?

sum.js (updated)

class Calc {
    add(a,b) {
        return a + b*3;
    }
}

foo.spec.js (updated)

const { test, expect } = require('@playwright/test');

test.describe('simple suite test', () => {
    test('unit test sum.js function', async ({ page }) => {

        await page.addScriptTag({path: 'sum.js'});
        const data = await page.evaluate(() => window.object.add(1,7));  // <- how to call the class function?
        await expect(data).toBe(22); // 1+7*3=22
        
    });
 });

Solution

  • The example is a bit contrived--it's extremely uncommon to need to inject a class into the website. If you want to unit test Node code without a browser, simply export the class and call Calc.add directly in Node context:

    // sum.js
    export class Calc {
      static add(a, b) {
        return a + b;
      }
    }
    
    // sum.test.js (run with npx playwright test)
    import {expect, test} from "@playwright/test"; // ^1.46.1
    import {Calc} from "./sum";
    
    test("adds two numbers", () => {
      expect(Calc.add(1, 7)).toBe(8);
    });
    

    Generally, use use Jest, vitest or Mocha for this--Playwright is for browser automation, so it's bringing in a lot of extra weight just to run a plain Node unit test. This may be acceptable if you're already using Playwright for testing a website, but keep in mind its non-web assertions and mocks are far less comprehensive and mature than Jest, and will only work for fairly trivial apps. As such, I'd still recommend using Jest for your non-browser automation tests.

    That said, I'll assume for the rest of the post that the use case is a more meaningful utility function that actually needs to run in the browser for some reason.

    The point of Playwright testing is to operate the site as the user does, without injecting a bunch of custom code that the user won't have access to.

    Generally run utility work in Node, outside of the site you're testing, especially if it doesn't interact with the DOM. Try to keep your evaluate blocks as minimal as possible. It's best to get the data back to Node as quickly as possible and work with it there.

    context.exposeFunction is another useful tool in certain cases, but if you're leaning on it heavily, that might be a red flag that your design is suboptimal. It should seldom be necessary to call Node functions from the browser context.

    Given those caveats and considerations, there are a few ways to inject a class into the browser.

    One way is to assign your class to the window so it's available in the global scope:

    // sum.js
    window.Calc = class Calc {
      static add(a, b) {
        return a + b;
      }
    };
    
    // sum.test.js
    import {expect, test} from "@playwright/test"; // ^1.46.1
    
    test.describe("simple suite test", () => {
      test("sum.js adds two numbers", async ({page}) => {
        await page.addScriptTag({path: "sum.js"});
        const sum = await page.evaluate(() => window.Calc.add(1, 7))
        await expect(sum).toBe(8);
      });
    });
    

    If you don't need to create an instance of the class, you can also use a plain object:

    // sum.js
    window.Calc = {
      add(a, b) {
        return a + b;
      }
    };
    

    If you do need an instance, a classical prototype function works:

    function Calc() {};
    Calc.prototype.add = function (a, b) {
      return a + b;
    };
    

    ...and modify the test code to use new window.Calc().add(1, 7).

    Another approach is stringifying the class, which can work even if the code is running in your test program's memory already, or if you cannot modify the sum.js source code as I did above. Although it's written for Puppeteer, this answer will work in Playwright too since the APIs for exposing code to the browser are nearly identical at the time of writing.