Javascript testing with mocha the html5 file api?

1.2k views Asked by At

I have simple image uploader in a website and a javascript function which uses FileReader and converts image to base64 to display it for user without uploading it to actual server.

function generateThumb(file) {
    var fileReader = new FileReader();
    fileReader.readAsDataURL(file);
    fileReader.onload = function (e) {
        // Converts the image to base64
        file.dataUrl = e.target.result;
    };
}

Now I am trying to write tests for this method using Mocha and Chai. My thinking is that I want to check if the file.dataUrl was successfully created and it is base64. So I would like to mock the local file somehow in testing environment (not sure how to do this). Or I should not test this at all and assume that this is working ?

1

There are 1 answers

0
Espen On BEST ANSWER

The answer here depends a little. Does your "generateThumbs" method have other logic than loading a files contents and assign that to a property of a passed in object? Or does it have other logic such as generating the thumbnail from the image data, reading out file properties and assigning them to the file object? and so on?

If so then I would infact suggest you mock out the FileReader object with your own, so that you can control your test results. However, it isn't the FileReaders functionality you want to test, it is your own logic. So you should assume that FileReader works and test that your code that depends upon it works.

Now since the method you posted was a bit on the small side, In that case I would just assume it works, rename the method and work on testing the rest of your code. But there is a place for having such a mock, and I must admit it was quite fun to figure out how to mock the event target, so I will give an example here, using your method as a basis:

//This implements the EventTarget interface
//and let's us control when, where and what triggers events
//and what they return
//it takes in a spy and some fake return data

var FakeFileReader = function(spy, fakeData) {
    this.listeners = {};
    this.fakeData = fakeData;
    this.spy = spy;
    this.addEventListener('load', function () {
        this.spy.loaded = true;
    });
};
//Fake version of the method we depend upon
FakeFileReader.prototype.readAsDataURL = function(file){
    this.spy.calledReadAsDataURL = true;
    this.spy.file = file;
    this.result = this.fakeData;
    this.dispatchEvent({type:'load'}); //assume file is loaded, and send event
};
FakeFileReader.prototype.listeners = null;
FakeFileReader.prototype.addEventListener = function(type, callback) {
    if(!(type in this.listeners)) {
        this.listeners[type] = [];
    }
    this.listeners[type].push(callback);
};

FakeFileReader.prototype.removeEventListener = function(type, callback) {
    if(!(type in this.listeners)) {
        return;
    }
    var stack = this.listeners[type];
    for(var i = 0, l = stack.length; i < l; i++) {
        if(stack[i] === callback){
            stack.splice(i, 1);
            return this.removeEventListener(type, callback);
        }
    }
};

FakeFileReader.prototype.dispatchEvent = function(event) {
    if(!(event.type in this.listeners)) {
        return;
    }
    var stack = this.listeners[event.type];
    event.target = this;
    for(var i = 0, l = stack.length; i < l; i++) {
        stack[i].call(this, event);
    }
};

// Your method

function generateThumb(file, reader){
    reader.readAsDataURL(file);
    reader.addEventListener('load', function (e) {
        file.dataUrl = base64(e.target.result);
    });

}

Now we have a nice little object that behaves like a FileReader, but we control it's behavior, and we can now use our method and inject it into, thus enabling us to use this mock for testing and the real thing for production.

So we can now write nice unit tests to test this out: I assume you have mocha and chai setup

describe('The generateThumb function', function () {
    var file = { src: 'image.file'};
    var readerSpy = {};
    var testData = 'TESTDATA';
    var reader    = new FakeFileReader(readerSpy, testData);
    it('should call the readAsDataURL function when given a file name and a FileReader', function () {
        generateThumb(file, reader);
        expect(readerSpy.calledReadAsDataURL).to.be.true;
        expect(readerSpy.loaded).to.be.true;
    });
    it('should load the file and convert the data to base64', function () {
        var expectedData = 'VEVTVERBVEE=';
        generateThumb(file, reader);
        expect(readerSpy.file.src).to.equal(file.src);
        expect(readerSpy.file.dataUrl).to.equal(expectedData);
    });
});

Here is a working JSFiddle example: https://jsfiddle.net/workingClassHacker/jL4xpwwv/2/

The benefits here are you can create several versions of this mock:

  • what happens when correct data is given?
  • what happens when incorrect data is given?
  • what happens when callback is never called?
  • what happens when too large of a file is put in?
  • what happens when a certain mime type is put in?

You can offcourse massively simplify the mock if all you need is that one event:

function FakeFileReader(spy, testdata){
    return {
        readAsDataURL:function (file) {
            spy.file = file;
            spy.calledReadAsDataURL = true;
            spy.loaded = true;
            this.target = {result: testdata};
            this.onload(this);
        }
    };
}

function generateThumb(file, reader){
    reader.onload = function (e) {
        file.dataUrl = base64(e.target.result);
    };
    reader.readAsDataURL(file);
}

Which is how I would actually do this. And create several of these for different purposes.

Simple version: https://jsfiddle.net/workingClassHacker/7g44h9fj/3/