Skip to content

Instantly share code, notes, and snippets.

@fragsalat
Last active June 25, 2018 14:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fragsalat/0a33295f0340e7e047ef5a820d8d2245 to your computer and use it in GitHub Desktop.
Save fragsalat/0a33295f0340e7e047ef5a820d8d2245 to your computer and use it in GitHub Desktop.
This is an example mock for aurelia-http-client. It provides the ability to expect outgoing calls and emulate the response to test the correct behavior of an function. The expectation can validate the URL, request method, request headers and request body.
import {HttpClientMock} from 'test/unit/mocks/http/http-client-mock';
import {reportUnfulfilled, reportUnexpected} from 'test/unit/mocks/http/mock-helper';
import {SomeViewModel} from 'app/view/some-view';
describe('A ViewModel', function() {
/** @type Element */
let host;
/** @type HttpClientMock */
let http;
/** @type SomeViewModel */
let viewModel;
beforeEach(function() {
host = document.createElement('div');
viewModel = new SomeViewModel(host, {tr: () => ''});
http = new HttpClientMock();
http.registerGlobally();
});
afterEach(function() {
reportUnfulfilled(expect, http);
reportUnexpected(expect, http);
});
it('calls the backend', function(done) {
viewModel.someData = [1,2,3];
// Validate the request goes with correct method, headers and body out
// and emulate the response with code 201 and Location header
http.expect('/some-backend-service/resource')
.withMethod('POST')
.withRequestHeader('Content-Type', 'application/json')
.withRequestBody(viewModel.someData)
.withResponseStatus(201)
.withResponseHeader('Location', 'http://result/url');
viewModel.createSomething(new Event('click')).then(done);
});
});
import {Expectation} from 'app/test/mocks/http/expectation';
export class ExpectationBuilder {
/**
* @param handler {RequestHandler}
*/
constructor(handler) {
this.expectation = new Expectation();
// Set reference to handler
handler.expect(this.expectation);
}
/**
* Expect an url to be called
* @param url {String}
* @returns {ExpectationBuilder}
*/
withUrl(url) {
this.expectation.url = url;
return this;
}
/**
* Expect the request to be done with a specific method
* @param method {String}
* @returns {ExpectationBuilder}
*/
withMethod(method) {
this.expectation.method = method;
return this;
}
/**
* Expect the request to contain certain header
* @param name {String}
* @param value {String}
* @returns {ExpectationBuilder}
*/
withRequestHeader(name, value) {
this.expectation.requestHeaders[name.toLocaleLowerCase()] = value;
return this;
}
/**
* Expect the request to send specific data
* @param body {String}
* @returns {ExpectationBuilder}
*/
withRequestBody(body) {
this.expectation.requestBody = typeof body === 'string' ? body : json(body);
return this;
}
/**
* Expect the request to respond with specific http status code
* @param status {Number}
* @returns {ExpectationBuilder}
*/
withResponseStatus(status) {
this.expectation.responseStatus = status;
return this;
}
/**
* Expect the response to contain a certain header
* @param name {String}
* @param value {String}
* @returns {ExpectationBuilder}
*/
withResponseHeader(name, value) {
this.expectation.responseHeaders[name.toLocaleLowerCase()] = value;
return this;
}
/**
* Expect the response to contain specific data
* @param body {String|Object}
* @returns {ExpectationBuilder}
*/
withResponseBody(body) {
this.expectation.responseBody = typeof body === 'string' ? body : json(body);
return this;
}
}
export class Expectation {
url = null;
method = null;
requestHeaders = {};
requestBody = null;
responseStatus = 200;
responseHeaders = {};
responseBody = '';
/**
* @param xhr {XHRMock}
* @returns {Boolean}
*/
matches(xhr) {
if (this.url && xhr.url !== this.url) {
return false;
}
if (this.method && xhr.method !== this.method) {
return false;
}
if (!this._compareObjects(this.requestHeaders, xhr.requestHeaders)) {
return false;
}
return !(this.requestBody && xhr.requestText !== this.requestBody);
}
/**
* @param object1 {Object}
* @param object2 {Object}
* @returns {Boolean}
* @private
*/
_compareObjects(object1, object2) {
if (Object.keys(object1).length) {
for (let key in object1) {
if (!object1.hasOwnProperty(key)) {
continue;
}
if (!object2[key] || object2[key] !== object1[key]) {
return false;
}
}
}
return true;
}
}
import {Container} from 'aurelia-dependency-injection';
import * as http from 'aurelia-http-client';
import {request as XHRMock} from 'mock-xhr';
function createMockMessageProcessor(xhr) {
return new http.RequestMessageProcessor(xhr, [
http.timeoutTransformer,
http.credentialsTransformer,
http.progressTransformer,
http.responseTypeTransformer,
http.headerTransformer,
http.contentTransformer
]);
}
export class HttpClientMock extends http.HttpClient {
/**
* @constructor
*/
constructor() {
super();
let handler = this.handler = new RequestHandler();
// Ensure each http client has it's own xhr mock instance with it's own handler
function XHR() {
XHRMock.apply(this, arguments);
this.upload = {};
this.onsend = () => handler.handle(this);
}
XHR.prototype = XHRMock.prototype;
// Create message processor with xhr mock
this.requestProcessorFactories = new Map();
this.requestProcessorFactories.set(http.HttpRequestMessage, () => createMockMessageProcessor(XHR));
this.requestProcessorFactories.set(http.JSONPRequestMessage, () => createMockMessageProcessor(XHR));
}
/**
* @void
*/
registerGlobally() {
new Container().makeGlobal();
Container.instance.registerInstance(http.HttpClient, this);
}
/**
* @param url {String}
* @returns {ExpectationBuilder}
*/
expect(url) {
return new ExpectationBuilder(this.handler).withUrl(url);
}
/**
* @returns {Boolean}
*/
isDone() {
return this.handler.isDone();
}
/**
* @returns {Array<Expectation>}
*/
getExpected() {
return this.handler.expected;
}
/**
* @returns {Boolean}
*/
hadUnexpected() {
return this.handler.hadUnexpected();
}
/**
* @returns {Array<XHRMock>}
*/
getUnexpected() {
return this.handler.unexpected;
}
}
export function json(obj) {
return JSON.stringify(obj);
}
export function reportUnfulfilled(expect, httpMock) {
let expectedUrls = httpMock.getExpected().map(stringifyXhr);
expect(httpMock.isDone()).toBeTruthy('Never called urls: \n' + expectedUrls.join('\n'));
}
export function reportUnexpected(expect, httpMock) {
let unexpectedUrls = httpMock.getUnexpected().map(stringifyXhr);
expect(httpMock.hadUnexpected()).toBeFalsy('Unexpected calls: \n' + unexpectedUrls.join('\n'));
}
function stringifyXhr(xhr) {
return `${xhr.method}: ${xhr.url}
Sent ${stringifyHeaders(xhr.requestHeaders)}
${xhr.requestText || xhr.requestBody}
Received ${stringifyHeaders(xhr.responseHeaders)}
${xhr.responseText || xhr.responseBody}`;
}
function stringifyHeaders(headers) {
let keys = Object.keys(headers);
let headerRepresentations = keys.map(key => `${key}: ${headers[key]}`);
return keys.length ? `Headers ${headerRepresentations.join(' | ')}` : '';
}
class RequestHandler {
/**
* @type {Array<Expectation>}
*/
expected = [];
/**
* @type {Array<XHRMock>}
*/
unexpected = [];
/**
* @param expected {Expectation}
*/
expect(expected) {
this.expected.push(expected);
}
/**
* @param xhr {XHRMock}
*/
handle(xhr) {
for (let i = this.expected.length; i--;) {
let expected = this.expected[i];
// Compare url and http method
if (expected.matches(xhr)) {
// Set expected response headers on xhr mock
xhr.responseHeaders = expected.responseHeaders || {};
// Emulate response with expected values
xhr.receive(expected.responseStatus, expected.responseBody);
// Remove the expected request because we handle it only once
this.expected.splice(i, 1);
return;
}
}
// No expected request was found if the function hasn't returned yet
this.unexpected.push(xhr);
}
/**
* @returns {Boolean}
*/
isDone() {
return !this.expected.length;
}
/**
* @returns {Boolean}
*/
hadUnexpected() {
return !!this.unexpected.length;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment