Mocking the server

Now that we have fixed our existing tests, we are ready to start writing tests for our real service, the fetchSpeakerService. Let's get started by looking at the test we used for our mock service. The tests will largely be the same as we are trying to achieve the same pattern of functionality.

First, we will want to create the test file fetchSpeakerService.spec.js. Once the file is created, we can add the standard existence test:

describe('Fetch Speaker Service', () => {
it('exits', () => {
expect(FetchSpeakerService).to.exist;
});
});

Because we stubbed out the fetch speaker service earlier, this test should just pass after we add the appropriate import.

Following the mock speaker service tests, the next test is a construction and type verification test:

it('can be constructed', () => {
// arrange
let service = new FetchSpeakerService();

// assert
expect(service).to.be.an.instanceof(FetchSpeakerService);
});

This test, too, should pass right away, because when we stubbed the fetch service we created it as a class. Continuing to follow the progression of the mock service tests, we have an After Initialization section with a Create section inside it. The only test in the Create section is an exists test for the Create method. Writing this test, it should pass:

describe('After Initialization', () => {
let service = null;

beforeEach(() => {
service = new FetchSpeakerService();
});

describe('Create', () => {
it('exists', () => {
expect(service.create).to.exist;
});
});
});

Because we are copying the flow from the mock service tests, we have already extracted the service to a beforeEach instantiation.

In the next section, our tests will start to get interesting and won't just pass right away. Before we move on, to verify that the tests are doing what they should be doing, it is a good idea to comment out parts of the fetch service and see the appropriate tests pass.

Moving on to the Get All section, still inside the After Initialization section, we have an existence test checking the getAll method:

describe('Get All', () => {
it('exists', () => {
// assert
expect(service.getAll).to.exist;
});
});

As with the other tests so far, to fail this test you will have to comment out the getAll method in the fetch service to see it fail. Immediately following this test are two more sections: No Speakers Exist and Speaker Listing. We will add them one at a time starting with No Speakers Exist:

describe.skip('No Speakers Exist', () => {
it('returns an empty array', () => {
// act
let promise = service.getAll();

// assert
return promise.then((result) => {
expect(result).to.have.lengthOf(0);
});
});
});

Finally, we have a failing test. The failure is complaining because it doesn't look like we returned a promise. Let's begin the proper implementation of the fetch service and we will use Sinon in the tests to mock the back-end. In the fetch service, add the following:

constructor(baseUrl) {
super();

this.baseUrl = baseUrl;
}

getAll() {
return fetch(`${this.baseUrl}/speakers`).then(r => {
return r.json();
});
}

This is a very basic fetch call. We are use the HTTP verb, GET, so there is no reason to call a method on fetch; by default it will use GET.

In our tests, we are now getting a meaningful result. fetch is not defined. This result is because fetch does not exist as part of our testing setup yet. We will need to import a new NPM package to handle fetch calls in testing. The package we want to import is fetch-ponyfill.

>npm install fetch-ponyfill

After installing the ponyfill library, we must modify our test setup file scripts/test.js:

import { JSDOM } from'jsdom';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import fetchPonyfill from 'fetch-ponyfill';
const { fetch } = fetchPonyfill();

const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;
window.fetch = window.fetch || fetch;



global.window = window;
global.document = window.document;
global.fetch = window.fetch;

After those modifications, we must restart our tests for the changes to take effect. We are now getting a test failure telling us that only absolute URLs are supported. We are getting this message because when we instantiate our fetch service we aren't passing a baseURL. For the tests it doesn't matter what the URL is so let's just use localhost:

beforeEach(() => {
service = new FetchSpeakerService('http://localhost');
});

After making this change we have moved the error forward and now we are getting a fetch error to the effect that localhost refused a connection. We are now ready to replace the back-end with Sinon.  We will start in the beforeEach and afterEach:

let fetch = null;

beforeEach(() => {
fetch = sinon.stub(global, 'fetch');
service = new FetchSpeakerService('http://localhost');
});

afterEach(() => {
fetch.restore();
});

In the test, we will need some items from the fetch-ponyfill package so let's add the import statements while we are close to the top of the file.

import fetchPonyfill from 'fetch-ponyfill';
const {
Response,
Headers
} = fetchPonyfill();

And now in the test, we need to configure the response from the server:

it('returns an empty array', () => {
// arrange
fetch.returns(new Promise((resolve, reject) => {
let response = new Response();
response.headers = new Headers({
'Content-Type': 'application/json'
});
response.ok = true;
response.status = 200;
response.statusText = 'OK';
response.body = JSON.stringify([]);

resolve(response);
}));

// act
let promise = service.getAll();

// assert
return promise.then((result) => {

     expect(result).to.have.lengthOf(0);
});
});

That finishes the No Speakers Exist scenario. We will refactor the server response once we have a better idea about what data will be changing.

We are now ready for the speaker listing scenario. As before, we start by copying the test from the mock service tests. Remove the arrange from the mock service test and copy the arrange from our previous test.

After adding the arrange from the no speakers test, we get a message expecting a length of 1 instead of 0. This is an easy fix and for the purposes of this test, we can simply add an empty object to the body array of the response. Here is what the test should look like, once it is passing:

describe('Speaker Listing', () => {
it('returns speakers', () => {
// arrange
fetch.returns(new Promise((resolve, reject) => {
let response = new Response();
response.headers = new Headers({
'Content-Type': 'application/json'
});
response.ok = true;
response.status = 200;
response.statusText = 'OK';
response.body = JSON.stringify([{}]);

resolve(response);
}));

// act
let promise = service.getAll();

// assert
return promise.then((result) => {
expect(result).to.have.lengthOf(1);
});
});
});
});

Now that we are using basically the same arrange twice, it's time to refactor our tests. The only thing that has really changed is the body. Let's extract an okResponse function to use:

function okResponse(body) {
return new Promise((resolve, reject) => {
let response = new Response();
response.headers = new Headers({
'Content-Type': 'application/json'
});
response.ok = true;
response.status = 200;
response.statusText = 'OK';
response.body = JSON.stringify(body);

resolve(response);
});
}

We have placed this helper function at the top of the After Initialization describe. Now in each test, replace the arrange with a call to the function, passing in the body that is specific to that test.

The get all speakers functionality is now covered by the tests. Let's move on to getting a specific speaker by ID. Copy the tests for getById from the mock service tests and apply a skip to the describes. Now, remove the skip from the outer-most describe. This should enable the existence test, which should pass.

The next test is for when a speaker is not found; removing skip from that test results in a message indicating that we are not returning a promise. Let's go into the body of the getById function and use fetch to get a speaker:

getById(id) {
return fetch(`${this.baseUrl}/speakers/${id}`);
}

Adding fetch to our function should have fixed the error but hasn't. Remember we are mocking the response from fetch so if we don't set a response then fetch won't return anything at all. Let's configure the mock response. In this case we are expecting a 404 from the server so let's configure that response:

// arrange
fetch.returns(new Promise((resolve, reject) => {
let response = new Response();
response.headers = new Headers({
'Content-Type': 'application/json'
});
response.ok = false;
response.status = 404;
response.statusText = 'NOT FOUND';

resolve(response);
}));

That makes our test pass, but it's not for the right reason. Let’s add a then clause to the assertion to prove the false positive:

// assert
return promise}).then(() => {
throw { type: 'Error not returned' };
}).catch((error) => {
expect(error.type).to.equal(errorTypes.SPEAKER_NOT_FOUND);
});

Now our test will fail with expected 'Error not returned' to equal 'SPEAKER_NOT_FOUND'. Why is this? Shouldn't a 404 cause a rejection of the promise? With fetch, the only thing that will cause a rejected promise is a network connection error. For that reason, we didn't reject when we mocked the server response. What we need to do is check for that condition in the service and cause a promise rejection on that side. The easiest way to accomplish this is to wrap the fetch call with a promise of our own. Once wrapped, we can check for the appropriate condition and reject our promise:

getById(id) {
return new Promise((resolve, reject) => {
fetch(`${this.baseUrl}/speakers/${id}`).then((response) => {
if (!response.ok) {
reject({
type: errorTypes.SPEAKER_NOT_FOUND
});
}
});
});
}

That should do it for this test. We are now ready for our last test. Before we move on, let's do a quick refactoring of the arrange in this test to shorten the test and have it make a bit more sense to future readers. While we are doing that, we will refactor the existing response function to reduce duplication and enforce some default values:

function baseResponse() {
let response = new Response();
response.headers = new Headers({
'Content-Type': 'application/json'
});
response.ok = true;
response.status = 200;
response.statusText = 'OK';

return response;
}

function okResponse(body) {
return new Promise((resolve, reject) => {
let response = baseResponse();
response.body = JSON.stringify(body);

resolve(response);
});
}

function notFoundResponse() {
return new Promise((resolve, reject) => {
let response = baseResponse();
response.ok = false;
response.status = 404;
response.statusText = 'NOT FOUND';

resolve(response);
})
}

Use the notFoundResponse function in the test just like we used the okResponse function. Moving on to our last test for the current functionality of the fetch service, remove the skip from the next describe and we will begin looking at the errors generated and make the necessary changes to make the test pass.

This last test is fairly simple after the work we have already done to make mock responses easier. We need the fetch call to return an ok response with the speaker as the body:

describe('Speaker Exists', () => {
it('returns the speaker', () => {
// arrange
const speaker = {
id: 'test-speaker'
};
fetch.returns(okResponse(speaker));

// act
let promise = service.getById('test-speaker');

// assert
return promise.then((speaker) => {
expect(speaker).to.not.be.null;
expect(speaker.id).to.equal('test-speaker');
});
});
});

Now, we are getting a timeout error. That is because our service isn't actually handling the case where the speaker exists. Let's add that now:

getById(id) {
return new Promise((resolve, reject) => {
fetch(`${this.baseUrl}/speakers/${id}`).then((response) => {
if (response.ok) {
resolve(response.json());
} else {
reject({
type: errorTypes.SPEAKER_NOT_FOUND
});
}
});
});
}

Now all our tests are passing and we have verified all the expected behavior of the system. There are a few more things we could do and some developers will choose to do them. We will discuss some of them but will not be providing examples.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.253.62