Manu Chadha
Manu Chadha

Reputation: 16723

how to mock failure of FileReader

I have a function which creates a FileReader. In that function I also set the load and error event handlers

handleFileSelect(files:ArrayLike<File>){
...
      let reader = new FileReader()
      reader.onload = this.handleReaderLoaded;
      reader.onerror = this.handleReaderError;


      reader.readAsDataURL(file);
    }
  }

I want to unit-test that handleFileSelect correctly sets the error handler and that the error handler (handleReaderError) gets called if FileReader fails. But I can't figure out how to make the FileReader fail.

The spec I have written so far is

fit('should call error handler when file doesn\'t get loaded successfully', (done) => {
    let newPracticeQuestionComponent = component;

    let file1 = new File(["foo1"], "foo1.txt");
    /*
    File reader will load the file asynchronously.
    The `done` method of `Jasmine` makes `Jasmine` wait
    When handleReaderError is called, call a fake function and within it call done
     */
    spyOn(newPracticeQuestionComponent,'handleReaderError').and.callFake(function(event:FileReaderProgressEvent){
      console.log("called fake implementation of handleReaderError ",event);
      expect(event.type).toEqual("abort");
      done();
    });

    newPracticeQuestionComponent.handleFileSelect([file1]);
//I SHOULD SIMULATE FILEREADER ERROR HERE BUT HOW??

  });

Upvotes: 3

Views: 3528

Answers (3)

Samoth
Samoth

Reputation: 1707

When you have a typescript file and a severity to be set depending on the success of reading an inputs file from disk:

enum Severity
{
  NONE = 'none',
  ERROR = 'danger',
  OK = 'success'
}

private jsonFile: File | undefined = undefined;
private severity: Severity = Severity.NONE;

protected changeFileSelection(): void
{
  this.jsonFile = this.input.files?.[0];

  const fileReader: FileReader = new FileReader();
  fileReader.onload = () => this.load(JSON.parse(fileReader.result as string));
  fileReader.onerror = (error: ProgressEvent<FileReader>): void => {
    this.severity = Severity.ERROR;
    console.error(error);
  };
  this.severity = Severity.OK;
  fileReader.readAsText(this.jsonFile, 'UTF-8');
}

You can test the onerror call in Jasmine as follows:

it('should handle file read errors', () => {
  const emptyFile: File = new File(['{}'], 'filename.json');
  component['jsonFile'] = emptyFile;
  const dataTransfer: DataTransfer = new DataTransfer();
  dataTransfer.items.add(emptyFile);
  input.files = dataTransfer.files; // @see https://stackoverflow.com/a/68182158/959484
  const fileReader: FileReader = new FileReader();
  spyOn(window, 'FileReader').and.returnValue(fileReader);
  spyOn(fileReader, 'readAsText').and.callFake((): void => {
    expect(component['severity']).toEqual(component['Severity'].OK);
    fileReader.dispatchEvent(new Event('error'));
  });

  component['changeFileSelection']();

  expect(component['severity']).toEqual(component['Severity'].ERROR);
});

Note, that the readAsText call must be after the positive Severity result you want to have in case of a successful read. But in this test case, the outcome will be negative due to simulated Error while reading.

Upvotes: 0

Lana
Lana

Reputation: 1267

It was already said that we can mock readAsDataURL method and dispatch the error event from it. But your reader is a local variable in the function handleFileSelect. In order to access the reader, we can mock the FileReader constructor and get control of the created file reader instance. Here I use sinon for mocking:

// in your test:
...

// This is the reader instance that we have access
const reader = new FileReader()

// We throw an error in readAsArrayBuffer method of that instance
reader.readAsArrayBuffer = () => {
  reader.dispatchEvent(new Event('error'))
}

// Now make the constructor return our instance
sinon.stub(window, 'FileReader').returns(r)

// Now make your calls and assertions
...    

// Don't forget to restore the original constructor at the end
window.FileReader.restore()

Upvotes: 2

mixth
mixth

Reputation: 636

If the reader's behaviour is calling onerror when readAsDataURL fails, this should do:

spyOn(newPracticeQuestionComponent.reader, 'readAsDataURL').and.callFake(() => {
    newPracticeQuestionComponent.reader.onerror();
});

Since this will be running as a synchronous call, you can simplify the assertion at the end of the test (following a triple A) like this:

// Arrange
const newPracticeQuestionComponent = component;
spyOn(newPracticeQuestionComponent, 'handleReaderError');
spyOn(newPracticeQuestionComponent.reader, 'readAsDataURL').and.callFake(() => {
    newPracticeQuestionComponent.reader.onerror();
});
let file1 = new File(["foo1"], "foo1.txt");

// Act
newPracticeQuestionComponent.handleFileSelect([file1]);

// Assert
expect(newPracticeQuestionComponent.handleReaderError).toHaveBeenCalledWith({ type: 'abort' });

But I don't recommend expecting the parameter passes to the function, event.type, because it is the specification of another unit that we are not currently testing. (we are testing newPracticeQuestionComponent not the behaviour of reader calling an error with an event)


Mocking the behaviour of reader might not be the best way. It depends on what you want to test against the unit.

In case we want to go extremely independent, newPracticeQuestionComponent should know nothing about reader's behaviour even the callback error, the only thing this unit should know is to set the onerror callback, you can just assert that you set the onerror of reader correctly.

// Arrange
const newPracticeQuestionComponent = component;
spyOn(newPracticeQuestionComponent.reader, 'readAsDataURL');
let file1 = new File(["foo1"], "foo1.txt");

// Act
newPracticeQuestionComponent.handleFileSelect([file1]);

// Assert
expect(newPracticeQuestionComponent.reader.onerror).toBe(newPracticeQuestionComponent.handleReaderError);

I am no master about testing, but it seems to be pros and cons writing tests like the above and below examples upon many factors.

Hope this helps :)

Upvotes: 1

Related Questions