How to test img.onload using Jest?

While this was answered, I disagree with the purpose of the solution.

We shouldn't write tests to pass coverage, we should write tests that prove a point, and ensure pieces of code behave as expected.

The way to test it is to mock the Image constructor and replace it with something that will invoke the onload function.

describe('tet it', () => {

  it('test', done => {
    global.Image = class {
      constructor() {
        setTimeout(() => {
          this.onload(); // simulate success
        }, 100);
      }
    }

    const callback = status => {
      done()
    }

    funcToTest('some_image', callback)
  })
})

The method just assumes that the 'browser' will download the image within 100 ms, you can tweak the code for failure, or move parts of it to beforeEach if you need to share this behavior between tests.


If you have some logic in Image's onload event handler and want to test if it was applied correctly, you can actually create an image blob programmatically using Canvas.

Consider this function:

const imageDimensions = (
  file: File
): Promise<{ width: number; height: number }> =>
  new Promise((resolve, reject) => {
    const img = new Image()

    img.onload = () => {
      const { naturalWidth: width, naturalHeight: height } = img
      resolve({ width, height })
    }

    img.onerror = () => {
      reject('There is something wrong with this image')
    }

    img.src = URL.createObjectURL(file)
  })`

So given a File (blob) it returns a Promise which resolves to an object with dimensions. Or rejects with an error.

How do we test onload part? You can programmatically create a blob in your tests, but new Blob(['a'.repeat(10)], { type: 'image/jpeg' }) will not trigger onload, since this is not really an image.

No need to mock anything, use JSDOM or anything like that. Canvas to the rescue! This could be tested as simple as that:

describe('imageDimensions', () => {
  it('should resolve with correct dimensions given an IMAGE blob', done => {
    // although this canvas has no actual graphics, it is still an image and it contains image metadata, thus onload() will be fired
    const canvas = document.createElement('canvas')
    canvas.width = 10
    canvas.height = 10

    canvas.toBlob(
      async blob => {
        const { width, height } = await component.imageDimensions(blob)
        expect(width).toBe(10)
        expect(height).toBe(10)
        done()
      },
      'image/jpeg',
      0.1
    )
  })

  it('should reject with an error if the file provided does NOT seem to be an image', async () => {
    const file = new Blob(['a'.repeat(10)], { type: 'application/pdf' })

    try {
      await component.imageDimensions(file)
    } catch (error) {
      expect(error).toBe('There is something wrong with this image')
    }
  })
})

In this example the Canvas is being created and then converted to a blob, which is similar to that being generated by a browser when user has selected some image file.

P.S. this is both Jest and Jasmine compatible.


The function to be tested:

index.ts:

function funcToTest(imgUrl: string, callback: Function) {
  const img = new Image();
  img.src = imgUrl;

  img.onload = () => {
    callback(true);
  };

  img.onerror = e => {
    callback(false);
    console.log(e);
  };

  return img;
}

export { funcToTest };

  • Unit test solution 1:
/**
 * @jest-environment jsdom
 */

import { funcToTest } from './';

describe('test suites', () => {
  it('onload', done => {
    const callback = jest.fn(status => {
      expect(status).toBe(true);
      done();
    });

    const imageUrl = 'https://github.com/mrdulin';
    const img = funcToTest(imageUrl, callback);
    if (img.onload) {
      const event: any = {};
      img.onload(event);
    }
  });

  it('onerror', done => {
    const consoleLogSpyOn = jest.spyOn(console, 'log');
    const callback = jest.fn(status => {
      expect(status).toBe(false);
      done();
    });

    const imageUrl = 'https://github.com/mrdulin';
    const img = funcToTest(imageUrl, callback);
    if (img.onerror) {
      const event: any = { message: 'some error' };
      img.onerror(event);
      expect(consoleLogSpyOn).toBeCalledWith(event);
    }
    consoleLogSpyOn.mockRestore();
  });
});

Unit test result and coverage:

 PASS  src/stackoverflow/57092154/index.spec.ts
  test suites
    ✓ onload (8ms)
    ✓ onerror (8ms)

  console.log node_modules/jest-mock/build/index.js:860
    { message: 'some error' }

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 index.ts |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.821s

Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57092154

UPDATE: undeleted answer, unit test solution 2

You can use Object.defineProperty() method to create getter and setter for Image.prototype.onload method. Then, you can get the onload function in your test cases and execute it manually.

index.test.ts:

import { funcToTest } from './';

describe('57092154', () => {
  let onloadRef: Function | undefined;
  let onerrorRef: Function | undefined;
  beforeAll(() => {
    Object.defineProperty(Image.prototype, 'onload', {
      get() {
        return this._onload;
      },
      set(onload: Function) {
        onloadRef = onload;
        this._onload = onload;
      },
    });
    Object.defineProperty(Image.prototype, 'onerror', {
      get() {
        return this._onerror;
      },
      set(onerror: Function) {
        onerrorRef = onerror;
        this._onerror = onerror;
      },
    });
  });
  it('should handle onload event', () => {
    const callback = jest.fn();
    funcToTest('./test.png', callback);
    onloadRef!();
    expect(callback).toBeCalledWith(true);
  });

  it('should handle onerror event', () => {
    const callback = jest.fn();
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => 'suppress error');
    funcToTest('./test.png', callback);
    const mErr = new Error('network');
    onerrorRef!(mErr);
    expect(callback).toBeCalledWith(false);
    expect(logSpy).toBeCalledWith(mErr);
  });
});

unit test result:

 PASS  examples/57092154/index.test.ts
  57092154
    ✓ should handle onload event (4 ms)
    ✓ should handle onerror event

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 index.ts |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.741 s