Search code examples
reactjsunit-testingxmlhttprequestsinonchai

React ES6 with chai/sinon: Unit testing XHR in component


I have a React utility component that reads the contents of a URL:

'use strict';

export class ReadURL {

  getContent = (url) => {
      return new Promise((resolve, reject) => {
console.log('Promise')
          let xhr = new XMLHttpRequest();

          xhr.open("GET", url, false);
          xhr.onreadystatechange = () => {
console.log('onreadystatechange', xhr.readyState)
              if (xhr.readyState === 4) {
                  if (xhr.status === 200 || xhr.status == 0) {
console.log('200')
                      var allText = xhr.responseText;
                      resolve(allText);
                  } else {
                      reject('ajax error:' + xhr.status + ' ' + xhr.responseText);
                  }
              }
          };
          xhr.send(null);
      });
  };

}

I have been trying to use Sinon's useFakeXMLHttpRequest() to stub the xhr, but no matter how I try, I can't get it to actually process - It currently passes with a false positive, without ever receiving onreadystatechange event. I've tried with XHR and Axios packages as well as native XMLHttpRequest, with the request wrapped in a promise and not, a whole bunch of different tacks, and read untold blog posts, docs and SO questions and I'm losing the will to live... The component itself works perfectly.

I've managed to get tests working with promises and with stubbed module dependancies, but this has me stumped.

This is the test:

import chai, { expect } from 'chai';
import sinon, { spy } from 'sinon';

import {ReadURL} from './ReadURL';


describe('ReadURL', () => {

  beforeEach(function() {

    this.xhr = sinon.useFakeXMLHttpRequest();

    this.requests = [];
    this.xhr.onCreate = (xhr) => {
console.log('xhr created', xhr)
      this.requests.push(xhr);
    };

    this.response = 'Response not set';
  });

  afterEach(function() {
    this.xhr.restore();

    this.response = 'Response not set';
  });

  it('should get file content from an xhr request', () => {
    const readURL = new ReadURL(),
          url = 'http://dummy.com/file.js',
          urlContent = `<awe.DisplayCode
            htmlSelector={'.awe-login'}
            jsxFile={'/src/js/components/AncoaAwe.js'}
            jsxTag={'awe.Login'}
            componentFile={'/src/js/components/Login/Login.js'}
          />`;

    readURL.getContent(url).then((response) =>{
console.log('ReadURL-test response', response)
            expect(response).to.equal(urlContent);
          });


    window.setTimeout(() => {
console.log('ReadURL-test trigger response')
      this.requests[0].respond(200,
        {
          'Content-Type': 'application/json'
        },
        urlContent
      )
    , 10});

  });

});

The console.log('xhr created', xhr) is triggered, and output confirms that it's a sinon useFakeXMLHttpRequest request.

I have created a repo of the app with the bare minimum required to see the components functions: https://github.com/DisasterMan78/awe-testcase

I haven't got a sample online currently, as I don't know of any online sandboxes that run tests. If I can find a service for that I'll try to add a proof of failed concept.

Help me Obi Wan-Kenobi. You're my only hope!


Solution

  • Well, that was fun. I got back to it today after working on an older project, but it still took my far too long. Stupid, small syntactic errors. Of course...

    There were two critical errors.

    Firstly, in order for the promise to complete, done needs to be passed as a parameter to the function of the test's it() function:

    it('should get file content from an xhr request', (done) => {
      ...
    }
    

    Now we can complete the promise with done:

        ...
        readURL.getContent(url)
          .then((response) => {
            expect(response).to.equal(this.response.content);
            done();
          })
          .catch((error) => {
    console.log(error);
            done();
          });
        ...
    

    Somehow none of the documentation or articles I read seemed to flag this, but few of them were also dealing with promises, and my ExtractTagFromURL test, which also relies on promises did not require this, but I have confirmed it is absolutely critical to this test.

    With the promise working properly, timeouts are not required around the respond() call:

        ...
        this.requests[0].respond(this.response.status,
          {
            'Content-Type': this.response.contentType
          },
          this.response.content
        );
        ...
    

    And finally, my largest cock up, in order for the values of this for this.requests and the other shared properties to be properly set and read, the function parameter of beforeEach() and afterEach() need to use arrow syntax:

      ...
      beforeEach(() => {
        ...
      }
      ...
    

    The reduced test case on Github has been updated, passes and can be cloned from https://github.com/DisasterMan78/awe-testcase

    I guess this makes me Obi Wan-Kenobe, huh?

    Feel free to ask for clarification if needed!