Rework jasmine ajax to behave more like the jasmine clock and spys

- use an instance of MockAjax for nicer tests
- the only global install is `mockAjax`
- mockAjax has a `requests` to track requests that have been made
- mockAjax has a `stubs` to track stubs that have been registered
- use just `install` and `uninstall` to be consistent with clock
This commit is contained in:
slackersoft 2013-10-11 22:16:22 -07:00
parent 7900fc3e18
commit c2a02dbdf6
7 changed files with 314 additions and 365 deletions

View File

@ -31,28 +31,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
(function() {
var jasmineAjaxInterface = {
// Jasmine-Ajax interface
ajaxRequests: [],
ajaxStubs: [],
mostRecentAjaxRequest: function() {
if (ajaxRequests.length > 0) {
return ajaxRequests[ajaxRequests.length - 1];
} else {
return null;
}
},
clearAjaxRequests: function() {
ajaxRequests = [];
},
clearAjaxStubs: function() {
ajaxStubs = [];
}
};
function extend(destination, source) {
for (var property in source) {
destination[property] = source[property];
@ -60,20 +38,67 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
return destination;
}
if (typeof window === "undefined" && typeof exports === "object") {
extend(exports, jasmineAjaxInterface);
} else {
extend(window, jasmineAjaxInterface);
}
function MockAjax(global) {
var requestTracker = new RequestTracker(),
stubTracker = new StubTracker(),
realAjaxFunction = global.XMLHttpRequest,
mockAjaxFunction = fakeRequest(requestTracker, stubTracker);
// Fake XHR for mocking Ajax Requests & Responses
window.FakeXMLHttpRequest = function() {
ajaxRequests.push(this);
this.install = function() {
global.XMLHttpRequest = mockAjaxFunction;
};
extend(window.FakeXMLHttpRequest.prototype, window.XMLHttpRequest);
extend(window.FakeXMLHttpRequest.prototype, {
this.uninstall = function() {
global.XMLHttpRequest = realAjaxFunction;
};
this.stubRequest = function(url) {
var stub = new RequestStub(url);
stubTracker.addStub(stub);
return stub;
};
this.withMock = function(closure) {
this.install();
try {
closure();
} finally {
this.uninstall();
}
};
this.requests = requestTracker;
this.stubs = stubTracker;
}
function StubTracker() {
var stubs = [];
this.addStub = function(stub) {
stubs.push(stub);
};
this.reset = function() {
stubs = [];
};
this.findStub = function(url) {
for (var i = stubs.length - 1; i >= 0; i--) {
var stub = stubs[i];
if (stub.url === url) {
return stub;
}
}
};
}
function fakeRequest(requestTracker, stubTracker) {
function FakeXMLHttpRequest() {
requestTracker.track(this);
}
extend(FakeXMLHttpRequest.prototype, window.XMLHttpRequest);
extend(FakeXMLHttpRequest.prototype, {
requestHeaders: {},
open: function() {
@ -106,7 +131,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
this.params = data;
this.readyState = 2;
var stub = jasmine.Ajax.matchStub(this.url);
var stub = stubTracker.findStub(this.url);
if (stub) {
this.response(stub);
}
@ -161,81 +186,48 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
}
});
jasmine.Ajax = {
isInstalled: function() {
return jasmine.Ajax.installed === true;
},
assertInstalled: function() {
if (!jasmine.Ajax.isInstalled()) {
throw new Error("Mock ajax is not installed, use jasmine.Ajax.useMock()");
return FakeXMLHttpRequest;
}
},
useMock: function(closure) {
jasmine.Ajax.installMock();
try {
closure();
} finally {
jasmine.Ajax.uninstallMock();
}
},
function RequestTracker() {
var requests = [];
installMock: function() {
jasmine.Ajax.installTopLevel();
jasmine.Ajax.installed = true;
},
installTopLevel: function() {
jasmine.Ajax.mode = 'toplevel';
jasmine.Ajax.real = window.XMLHttpRequest;
window.XMLHttpRequest = window.FakeXMLHttpRequest;
},
uninstallMock: function() {
jasmine.Ajax.assertInstalled();
window.XMLHttpRequest = jasmine.Ajax.real;
jasmine.Ajax.reset();
},
reset: function() {
jasmine.Ajax.installed = false;
jasmine.Ajax.mode = null;
jasmine.Ajax.real = null;
},
stubRequest: function(url) {
var Stub = function(url) {
this.url = url;
this.track = function(request) {
requests.push(request);
};
Stub.prototype.andReturn = function(options) {
this.first = function() {
return requests[0];
};
this.count = function() {
return requests.length;
};
this.reset = function() {
requests = [];
};
this.mostRecent = function() {
return requests[requests.length - 1];
};
}
function RequestStub(url) {
this.url = url;
this.andReturn = function(options) {
this.status = options.status || 200;
this.contentType = options.contentType;
this.responseText = options.responseText;
};
var stub = new Stub(url);
ajaxStubs.push(stub);
return stub;
},
matchStub: function(url) {
for (var i = ajaxStubs.length - 1; i >= 0; i--) {
var stub = ajaxStubs[i];
if (stub.url === url) {
return stub;
}
if (typeof window === "undefined" && typeof exports === "object") {
exports.MockAjax = MockAjax;
} else {
window.MockAjax = MockAjax;
}
},
installed: false,
mode: null
};
}());

View File

@ -1,8 +1,13 @@
describe("FakeXMLHttpRequest", function() {
var xhr;
beforeEach(function() {
xhr = new FakeXMLHttpRequest();
var realXMLHttpRequest = jasmine.createSpy('realRequest'),
fakeGlobal = {XMLHttpRequest: realXMLHttpRequest},
mockAjax = new MockAjax(fakeGlobal);
mockAjax.install();
xhr = new fakeGlobal.XMLHttpRequest();
});
it("should have an initial readyState of 0 (uninitialized)", function() {
expect(xhr.readyState).toEqual(0);
});
@ -52,6 +57,7 @@ describe("FakeXMLHttpRequest", function() {
});
it("can be extended", function(){
pending("why do we want to do this?");
FakeXMLHttpRequest.prototype.foo = function(){
return "foo";
};

View File

@ -1,3 +1,3 @@
beforeEach(function() {
clearAjaxRequests();
});
// beforeEach(function() {
// clearAjaxRequests();
// });

View File

@ -1,116 +1,52 @@
describe("jasmine.Ajax", function() {
beforeEach(function() {
jasmine.Ajax.reset();
describe("mockAjax", function() {
it("does not replace XMLHttpRequest until it is installed", function() {
var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'),
fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest },
mockAjax = new MockAjax(fakeGlobal);
fakeGlobal.XMLHttpRequest('foo');
expect(fakeXmlHttpRequest).toHaveBeenCalledWith('foo');
fakeXmlHttpRequest.calls.reset();
mockAjax.install();
fakeGlobal.XMLHttpRequest('foo');
expect(fakeXmlHttpRequest).not.toHaveBeenCalled();
});
describe("isInstalled", function() {
it("returns true if the mock has been installed", function() {
jasmine.Ajax.installed = true;
expect(jasmine.Ajax.isInstalled()).toBeTruthy();
it("replaces the global XMLHttpRequest on uninstall", function() {
var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'),
fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest },
mockAjax = new MockAjax(fakeGlobal);
mockAjax.install();
mockAjax.uninstall();
fakeGlobal.XMLHttpRequest('foo');
expect(fakeXmlHttpRequest).toHaveBeenCalledWith('foo');
});
it("returns false if the mock has not been installed", function() {
jasmine.Ajax.installed = false;
expect(jasmine.Ajax.isInstalled()).toBeFalsy();
});
it("allows the httpRequest to be retrieved", function() {
var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'),
fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest },
mockAjax = new MockAjax(fakeGlobal);
mockAjax.install();
var request = new fakeGlobal.XMLHttpRequest();
expect(mockAjax.requests.count()).toBe(1);
expect(mockAjax.requests.mostRecent()).toBe(request);
});
describe("assertInstalled", function() {
it("doesn't raise an error if the mock is installed", function() {
jasmine.Ajax.installed = true;
expect(
function() {
jasmine.Ajax.assertInstalled();
}).not.toThrowError("Mock ajax is not installed, use jasmine.Ajax.useMock()");
});
it("allows the httpRequests to be cleared", function() {
var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'),
fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest },
mockAjax = new MockAjax(fakeGlobal);
it("raises an error if the mock is not installed", function() {
jasmine.Ajax.installed = false;
expect(
function() {
jasmine.Ajax.assertInstalled();
}).toThrowError("Mock ajax is not installed, use jasmine.Ajax.useMock()");
});
});
mockAjax.install();
var request = new fakeGlobal.XMLHttpRequest();
describe("installMock", function() {
describe("when using a top-level replacement", function() {
it("installs the mock", function() {
jasmine.Ajax.installMock();
expect(window.XMLHttpRequest).toBe(FakeXMLHttpRequest);
});
it("saves a reference to the browser's XHR", function() {
var xhr = window.XMLHttpRequest;
jasmine.Ajax.installMock();
expect(jasmine.Ajax.real).toBe(xhr);
});
it("sets mode to 'toplevel'", function() {
jasmine.Ajax.installMock();
expect(jasmine.Ajax.mode).toEqual("toplevel");
expect(mockAjax.requests.mostRecent()).toBe(request);
mockAjax.requests.reset();
expect(mockAjax.requests.count()).toBe(0);
});
});
it("sets the installed flag to true", function() {
jasmine.Ajax.installMock();
expect(jasmine.Ajax.installed).toBeTruthy();
});
});
describe("uninstallMock", function() {
describe("when using toplevel", function() {
it("returns ajax control to the browser object", function() {
var xhr = window.XMLHttpRequest;
jasmine.Ajax.installMock();
jasmine.Ajax.uninstallMock();
expect(window.XMLHttpRequest).toBe(xhr);
});
});
it("raises an exception if jasmine.Ajax is not installed", function() {
expect(function(){ jasmine.Ajax.uninstallMock(); }).toThrowError("Mock ajax is not installed, use jasmine.Ajax.useMock()");
});
it("sets the installed flag to false", function() {
jasmine.Ajax.installMock();
jasmine.Ajax.uninstallMock();
expect(jasmine.Ajax.installed).toBeFalsy();
// so uninstallMock doesn't throw error when spec.after runs
jasmine.Ajax.installMock();
});
it("sets the mode to null", function() {
jasmine.Ajax.installMock();
jasmine.Ajax.uninstallMock();
expect(jasmine.Ajax.mode).toEqual(null);
jasmine.Ajax.installMock();
});
});
describe("useMock", function() {
it("installs the mock and uninstalls when done", function() {
var realRequest = spyOn(window, 'XMLHttpRequest'),
fakeRequest = spyOn(window, 'FakeXMLHttpRequest');
expect(function() {
jasmine.Ajax.useMock(function() {
window.XMLHttpRequest();
throw "function that has an error"
});
}).toThrow();
expect(realRequest).not.toHaveBeenCalled();
expect(fakeRequest).toHaveBeenCalled();
fakeRequest.calls.reset();
window.XMLHttpRequest();
expect(realRequest).toHaveBeenCalled();
expect(fakeRequest).not.toHaveBeenCalled();
});
});
});

View File

@ -3,9 +3,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
var success, error, complete;
var client, onreadystatechange;
var sharedContext = {};
var fakeGlobal, mockAjax;
beforeEach(function() {
jasmine.Ajax.installMock();
var fakeXMLHttpRequest = jasmine.createSpy('realFakeXMLHttpRequest');
fakeGlobal = {XMLHttpRequest: fakeXMLHttpRequest};
mockAjax = new MockAjax(fakeGlobal);
mockAjax.install();
success = jasmine.createSpy("onSuccess");
error = jasmine.createSpy("onFailure");
@ -29,17 +33,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
};
});
afterEach(function() {
jasmine.Ajax.uninstallMock();
});
describe("when making a request", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.send();
request = mostRecentAjaxRequest();
request = mockAjax.requests.mostRecent();
});
it("should store URL and transport", function() {
@ -47,73 +47,74 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
});
it("should queue the request", function() {
expect(ajaxRequests.length).toEqual(1);
expect(mockAjax.requests.count()).toEqual(1);
});
it("should allow access to the queued request", function() {
expect(ajaxRequests[0]).toEqual(request);
expect(mockAjax.requests.first()).toEqual(request);
});
describe("and then another request", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.send();
anotherRequest = mostRecentAjaxRequest();
anotherRequest = mockAjax.requests.mostRecent();
});
it("should queue the next request", function() {
expect(ajaxRequests.length).toEqual(2);
expect(mockAjax.requests.count()).toEqual(2);
});
it("should allow access to the other queued request", function() {
expect(ajaxRequests[1]).toEqual(anotherRequest);
expect(mockAjax.requests.first()).toEqual(request);
expect(mockAjax.requests.mostRecent()).toEqual(anotherRequest);
});
});
describe("mostRecentAjaxRequest", function () {
describe("mockAjax.requests.mostRecent()", function () {
describe("when there is one request queued", function () {
it("should return the request", function() {
expect(mostRecentAjaxRequest()).toEqual(request);
expect(mockAjax.requests.mostRecent()).toEqual(request);
});
});
describe("when there is more than one request", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.send();
anotherRequest = mostRecentAjaxRequest();
anotherRequest = mockAjax.requests.mostRecent();
});
it("should return the most recent request", function() {
expect(mostRecentAjaxRequest()).toEqual(anotherRequest);
expect(mockAjax.requests.mostRecent()).toEqual(anotherRequest);
});
});
describe("when there are no requests", function () {
beforeEach(function() {
clearAjaxRequests();
mockAjax.requests.reset();
});
it("should return null", function() {
expect(mostRecentAjaxRequest()).toEqual(null);
expect(mockAjax.requests.mostRecent()).toBeUndefined();
});
});
});
describe("clearAjaxRequests()", function () {
beforeEach(function() {
clearAjaxRequests();
mockAjax.requests.reset();
});
it("should remove all requests", function() {
expect(ajaxRequests.length).toEqual(0);
expect(mostRecentAjaxRequest()).toEqual(null);
expect(mockAjax.requests.count()).toEqual(0);
expect(mockAjax.requests.mostRecent()).toBeUndefined();
});
});
});
@ -121,13 +122,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
describe("when simulating a response with request.response", function () {
describe("and the response is Success", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.setRequestHeader("Content-Type", "text/plain")
client.send();
request = mostRecentAjaxRequest();
request = mockAjax.requests.mostRecent();
response = {status: 200, contentType: "text/html", responseText: "OK!"};
request.response(response);
@ -154,13 +155,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
describe("and the response is Success, but with JSON", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.setRequestHeader("Content-Type", "application/json")
client.send();
request = mostRecentAjaxRequest();
request = mockAjax.requests.mostRecent();
var responseObject = {status: 200, contentType: "application/json", responseText: '{"foo":"bar"}'};
request.response(responseObject);
@ -194,13 +195,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
describe("the content type defaults to application/json", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.setRequestHeader("Content-Type", "application/json")
client.send();
request = mostRecentAjaxRequest();
request = mockAjax.requests.mostRecent();
response = {status: 200, responseText: '{"foo": "valid JSON, dammit."}'};
request.response(response);
@ -227,13 +228,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
describe("and the status/response code is 0", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.setRequestHeader("Content-Type", "text/plain")
client.send();
request = mostRecentAjaxRequest();
request = mockAjax.requests.mostRecent();
response = {status: 0, responseText: '{"foo": "whoops!"}'};
request.response(response);
@ -261,13 +262,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
describe("and the response is error", function () {
beforeEach(function() {
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.setRequestHeader("Content-Type", "text/plain")
client.send();
request = mostRecentAjaxRequest();
request = mockAjax.requests.mostRecent();
response = {status: 500, contentType: "text/html", responseText: "(._){"};
request.response(response);
@ -296,13 +297,13 @@ describe("Jasmine Mock Ajax (for toplevel)", function() {
beforeEach(function() {
clock.install();
client = new XMLHttpRequest();
client = new fakeGlobal.XMLHttpRequest();
client.onreadystatechange = onreadystatechange;
client.open("GET", "example.com/someApi");
client.setRequestHeader("Content-Type", "text/plain")
client.send();
request = mostRecentAjaxRequest();
request = mockAjax.requests.mostRecent();
response = {contentType: "text/html", responseText: "(._){"};
request.responseTimeout(response);

View File

@ -1,11 +1,12 @@
describe("Webmock style mocking", function() {
var successSpy, errorSpy, response;
var successSpy, errorSpy, response, fakeGlobal, mockAjax;
var sendRequest = function() {
var xhr = new XMLHttpRequest();
var sendRequest = function(fakeGlobal) {
var xhr = new fakeGlobal.XMLHttpRequest();
xhr.onreadystatechange = function(arguments) {
if (this.readyState == this.DONE) {
response = this;
successSpy();
}
};
@ -14,47 +15,44 @@ describe("Webmock style mocking", function() {
};
beforeEach(function() {
jasmine.Ajax.installMock();
jasmine.Ajax.stubRequest("http://example.com/someApi").andReturn({responseText: "hi!"});
successSpy = jasmine.createSpy('success');
fakeGlobal = {XMLHttpRequest: jasmine.createSpy('realXMLHttpRequest')};
mockAjax = new MockAjax(fakeGlobal);
mockAjax.install();
sendRequest();
mockAjax.stubRequest("http://example.com/someApi").andReturn({responseText: "hi!"});
});
afterEach(function() {
jasmine.Ajax.uninstallMock();
clearAjaxStubs();
it("allows a url to be setup as a stub", function() {
sendRequest(fakeGlobal);
expect(successSpy).toHaveBeenCalled();
});
it("should allow you to clear all the ajax stubs", function() {
expect(ajaxStubs.length).toEqual(1);
clearAjaxStubs();
expect(ajaxStubs.length).toEqual(0);
});
it("should push the new stub on the ajaxStubs", function() {
expect(ajaxStubs.length).toEqual(1);
});
it("should set the url in the stub", function() {
expect(ajaxStubs[0].url).toEqual("http://example.com/someApi");
mockAjax.stubs.reset();
sendRequest(fakeGlobal);
expect(successSpy).not.toHaveBeenCalled();
});
it("should set the contentType", function() {
sendRequest(fakeGlobal);
expect(response.responseHeaders['Content-type']).toEqual('application/json');
});
it("should set the responseText", function() {
sendRequest(fakeGlobal);
expect(response.responseText).toEqual('hi!');
});
it("should default the status to 200", function() {
sendRequest(fakeGlobal);
expect(response.status).toEqual(200);
});
describe("with another stub for the same url", function() {
beforeEach(function() {
jasmine.Ajax.stubRequest("http://example.com/someApi").andReturn({responseText: "no", status: 403});
sendRequest();
mockAjax.stubRequest("http://example.com/someApi").andReturn({responseText: "no", status: 403});
sendRequest(fakeGlobal);
});
it("should set the status", function() {
@ -65,25 +63,4 @@ describe("Webmock style mocking", function() {
expect(response.responseText).toEqual('no');
});
});
describe(".matchStub", function() {
it("should be able to find a stub with an exact match", function() {
var stub = jasmine.Ajax.matchStub("http://example.com/someApi");
expect(stub).toBeDefined();
});
describe("with another stub for the same url", function() {
beforeEach(function() {
jasmine.Ajax.stubRequest("http://example.com/someApi").andReturn({responseText: "no", status: 403});
});
it("should use the latest stub", function() {
var stub = jasmine.Ajax.matchStub("http://example.com/someApi");
expect(stub.status).toEqual(403);
expect(stub.responseText).toEqual('no');
});
});
});
});

View File

@ -0,0 +1,37 @@
describe("withMock", function() {
var sendRequest = function(fakeGlobal) {
var xhr = new fakeGlobal.XMLHttpRequest();
xhr.open("GET", "http://example.com/someApi");
xhr.send();
};
it("installs the mock for passed in function, and uninstalls when complete", function() {
var xmlHttpRequest = spyOn(window, 'XMLHttpRequest').and.returnValue({open: function() {}, send: function() {}}),
fakeGlobal = {XMLHttpRequest: xmlHttpRequest},
mockAjax = new MockAjax(fakeGlobal);
mockAjax.withMock(function() {
sendRequest(fakeGlobal);
expect(xmlHttpRequest).not.toHaveBeenCalled();
});
sendRequest(fakeGlobal);
expect(xmlHttpRequest).toHaveBeenCalled();
});
it("properly uninstalls when the passed in function throws", function() {
var xmlHttpRequest = spyOn(window, 'XMLHttpRequest').and.returnValue({open: function() {}, send: function() {}}),
fakeGlobal = {XMLHttpRequest: xmlHttpRequest},
mockAjax = new MockAjax(fakeGlobal);
expect(function() {
mockAjax.withMock(function() {
throw "error"
});
}).toThrow("error");
sendRequest(fakeGlobal);
expect(xmlHttpRequest).toHaveBeenCalled();
});
});