@qooxdoo/framework
Version:
The JS Framework for Coders
535 lines (495 loc) • 16.6 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2011 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Tristan Koch (tristankoch)
************************************************************************ */
/**
*
* Provides test spies, stubs and mocks as well as custom assertions.
*
* Here is a simple example:
*
* <pre class="javascript">
*
* // Test
* qx.Class.define("qx.test.Klass",
* {
* extend : qx.dev.unit.TestCase,
*
* include : qx.dev.unit.MMock,
*
* members :
* {
* "test: doSpecial on condition xyz": function() {
* // Set-Up
* var obj = new qx.Klass();
*
* // Wraps obj.doSpecial in a spy function and
* // replaces the original method with this spy.
* this.spy(obj, "doSpecial");
*
* // Run code that is expected to fulfill condition
* obj.onCondition("xyz");
*
* // Assert that spy was called
* this.assertCalled(obj.doSpecial);
* },
*
* tearDown: function() {
* // Restore all stubs, spies and overridden host objects.
* //
* // It is a good idea to always run this in the <code>tearDown()</code>
* // method, especially when overwriting global or host objects.
* this.getSandbox().restore();
* }
* }
* });
*
* // Implementation
* qx.Class.define("qx.Klass",
* {
* extend : qx.core.Object,
*
* members :
* {
* onCondition: function(condition) {
*
* // Complex code determining mustDoSpecial
* // by examining condition passed
*
* if (mustDoSpecial) {
* this.doSpecial();
* }
* },
*
* doSpecial: function() {
*
* }
* }
* });
*
* </pre>
*
* This mixin provides assertions such as assertCalled() that work
* with spies and stubs. Besides offering a compact way to express expectations,
* those assertions have the advantage that meaningful error messages can be
* generated.
*
* For full list of assertions see http://sinonjs.org/docs/#assertions.
* Note that sinon.assert.xyz() translates as assertXyz().
*
*/
qx.Mixin.define("qx.dev.unit.MMock",
{
construct: function()
{
var sinon = this.__getSinon();
this.__exposeAssertions();
this.__sandbox = sinon.sandbox;
},
members :
{
__sandbox: null,
__fakeXhr: null,
/**
* Expose Sinon.JS assertions. Provides methods such
* as assertCalled(), assertCalledWith().
* (http://sinonjs.org/docs/#assert-expose)
* Does not override existing assertion methods.
* @ignore(sinon.assert.expose)
*/
__exposeAssertions : function() {
var temp = {};
sinon.assert.expose(temp, {includeFail: false});
for (var method in temp) {
if (!this[method]) {
this[method] = temp[method];
}
}
},
/**
* Get the Sinon.JS object.
*
* @return {Object}
* @internal
*/
__getSinon: function() {
return qx.dev.unit.Sinon.getSinon();
},
/**
* Test spies allow introspection on how a function is used
* throughout the system under test.
*
* * spy()
* Creates an anonymous function that records arguments,
* this value, exceptions and return values for all calls.
*
* * spy(func)
* Spies on the provided function
*
* * spy(object, "method")
* Creates a spy for object.method and replaces the original method
* with the spy. The spy acts exactly like the original method in all cases.
* The original method can be restored by calling object.method.restore().
* The returned spy is the function object which replaced the original method.
* spy === object.method.
*
* * spy.withArgs(arg1[, arg2, ...])
* Creates a spy that only records calls when the received arguments matches those
* passed to <code>withArgs</code>.
*
* A spy has a rich interface to introspect how the wrapped function was used:
*
* * spy.withArgs(arg1[, arg2, ...]);
* * spy.callCount
* * spy.called
* * spy.calledOnce
* * spy.calledTwice
* * spy.calledThrice
* * spy.firstCall
* * spy.secondCall
* * spy.thirdCall
* * spy.lastCall
* * spy.calledBefore(anotherSpy)
* * spy.calledAfter(anotherSpy)
* * spy.calledOn(obj)
* * spy.alwaysCalledOn(obj)
* * spy.calledWith(arg1, arg2, ...)
* * spy.alwaysCalledWith(arg1, arg2, ...)
* * spy.calledWithExactly(arg1, arg2, ...)
* * spy.alwaysCalledWithExactly(arg1, arg2, ...)
* * spy.calledWithMatch(arg1, arg2, ...);
* * spy.alwaysCalledWithMatch(arg1, arg2, ...);
* * spy.calledWithNew();
* * spy.neverCalledWith(arg1, arg2, ...);
* * spy.neverCalledWithMatch(arg1, arg2, ...);
* * spy.threw()
* * spy.threw("TypeError")
* * spy.threw(obj)
* * spy.alwaysThrew()
* * spy.alwaysThrew("TypeError")
* * spy.alwaysThrew(obj)
* * spy.returned(obj)
* * spy.alwaysReturned(obj)
* * spy.getCall(n)
* * spy.thisValues
* * spy.args
* * spy.exceptions
* * spy.returnValues
* * spy.reset()
* * spy.printf("format string", [arg1, arg2, ...])
*
* See http://sinonjs.org/docs/#spies.
*
* Note: Spies are transparently added to a sandbox. To restore
* the original function for all spies run <code>this.getSandbox().restore()</code>
* in your <code>tearDown()</code> method.
*
* @param function_or_object {Function|Object} Spies on the
* provided function or object.
* @param method {String?null} The method to spy upon if an object was given.
* @return {Function} The wrapped function enhanced with properties and methods
* that allow for introspection. See http://sinonjs.org/docs/#spies.
*/
spy: function(function_or_object, method) {
return this.__sandbox.spy.apply(this.__sandbox, arguments);
},
/**
* Test stubs are functions (spies) with pre-programmed behavior.
*
* * stub()
* Creates an anonymous stub function
*
* * stub(object, "method")
* Replaces object.method with a stub function. The original function
* can be restored by calling object.method.restore() (or stub.restore()).
* An exception is thrown if the property is not already a function,
* to help avoid typos when stubbing methods.
*
* * stub(obj)
* Stubs all the object's methods.
*
* * stub.withArgs(arg1[, arg2, ...])
* Stubs the method only for the provided arguments. Can be used to create
* a stub that acts differently in response to different arguments.
*
* A stub has the interface of a spy in addition to methods that allow to define behaviour:
*
* * stub.returns(obj)
* * stub.throws()
* * stub.throws("TypeError")
* * stub.throws(obj)
* * stub.callsArg(index)
* * stub.callsArg(0)
* * stub.callsArgWith(index, arg1, arg2, ...)
*
* See http://sinonjs.org/docs/#stubs.
*
* Note: Stubs are transparently added to a sandbox. To restore
* the original function for all stubs run <code>this.getSandbox().restore()</code>
* in your <code>tearDown()</code> method.
*
* @param object {Object?null} Object to stub. Creates an anonymous stub function
* if not given.
* @param method {String?null} Replaces object.method with a stub function.
* An exception is thrown if the property is not already a function, to
* help avoid typos when stubbing methods.
* @return {Function} A stub. Has the interface of a spy in addition to methods
* that allow to define behaviour. See http://sinonjs.org/docs/#stubs.
*
*/
stub: function(object, method) {
return this.__sandbox.stub.apply(this.__sandbox, arguments);
},
/**
* Mocks are slightly different from spies and stubs in that you mock an
* object, and then set an expectation on one or more of its objects.
*
* * var mock = mock(obj)
* Creates a mock for the provided object. Does not change the object, but
* returns a mock object to set expectations on the object's methods.
*
* * var expectation = mock.expects("method")
* Overrides obj.method with a mock function and returns an expectation
* object. Expectations implement both the spy and stub interface plus
* the methods described below.
*
* Set expectations with following methods. All methods return the expectation
* itself, meaning expectations can be chained.
*
* * expectation.atLeast(number);
* * expectation.atMost(number);
* * expectation.never();
* * expectation.once();
* * expectation.twice();
* * expectation.thrice();
* * expectation.exactly(number);
* * expectation.withArgs(arg1, arg2, ...);
* * expectation.withExactArgs(arg1, arg2, ...);
* * expectation.on(obj);
* * expectation.verify();
*
* See http://sinonjs.org/docs/#mocks.
*
* @param object {Object} The object to create a mock of.
* @return {Function} A mock to set expectations on. See http://sinonjs.org/docs/#mocks.
*/
mock: function(object) {
var sinon = this.__getSinon();
return sinon.mock.apply(sinon, arguments);
},
/**
* Replace the native XMLHttpRequest object in browsers that support it with
* a custom implementation which does not send actual requests.
*
* Note: The fake XHR is transparently added to a sandbox. To restore
* the original host method run <code>this.getSandbox().restore()</code>
* in your <code>tearDown()</code> method.
*
* See http://sinonjs.org/docs/#useFakeXMLHttpRequest.
*
* @return {Object}
*/
useFakeXMLHttpRequest: function() {
return this.__fakeXhr = this.__sandbox.useFakeXMLHttpRequest();
},
/**
* Get requests made with faked XHR or server.
*
* Each request can be queried for url, method, requestHeaders,
* status and more.
*
* See http://sinonjs.org/docs/#FakeXMLHttpRequest.
*
* @return {Array} Array of faked requests.
*/
getRequests: function() {
return this.__fakeXhr.requests;
},
/**
* As {@link #useFakeXMLHttpRequest}, but additionally provides a high-level
* API to setup server responses. To setup responses, use the server
* returned by {@link #getServer}.
*
* See http://sinonjs.org/docs/#server.
*
* Note: The fake server is transparently added to a sandbox. To restore
* the original host method run <code>this.getSandbox().restore()</code>
* in your <code>tearDown()</code> method.
*
* @return {Object}
*/
useFakeServer: function() {
return this.__fakeXhr = this.__sandbox.useFakeServer();
},
/**
* Get fake server created by {@link #useFakeServer}.
*
* @return {Object} Fake server.
*/
getServer: function() {
return this.__sandbox.server;
},
/**
* Get sandbox.
*
* The sandbox holds all stubs and mocks. Run <code>this.getSandbox().restore()</code>
* to restore all mock objects.
*
* @return {Object}
* Sandbox object.
*/
getSandbox: function() {
return this.__sandbox;
},
/**
* EXPERIMENTAL - NOT READY FOR PRODUCTION
*
* Returns a deep copied, API-identical stubbed out clone of the given
* object.
*
* In contrast to the shallow {@link #stub}, also stubs out properties that
* belong to the prototype chain.
*
* @param object {Object} Object to stub deeply.
* @return {Object} A stub.
*/
deepStub: function(object) {
this.__getOwnProperties(object).forEach(function(prop) {
this.__stubProperty(object, prop);
}, this);
return object;
},
/**
* EXPERIMENTAL - NOT READY FOR PRODUCTION
*
* Shallowly stub all methods (except excluded) that belong to classes found in inheritance
* chain up to (but including) the given class.
*
* @param object {Object} Object to stub shallowly.
* @param targetClazz {Object} Class which marks the end of the chain.
* @param propsToExclude {Array} Array with properties which shouldn't be stubbed.
* @return {Object} A stub.
*/
shallowStub: function(object, targetClazz, propsToExclude) {
this.__getOwnProperties(object, targetClazz).forEach(function(prop) {
if (propsToExclude && propsToExclude.indexOf(prop) >= 0) {
// don't stub excluded prop
return;
}
this.__stubProperty(object, prop);
}, this);
return object;
},
/**
* EXPERIMENTAL - NOT READY FOR PRODUCTION
*
* Changes the given factory (e.g. a constructor) to return a stub. The
* method itself returns this stub.
*
* By default, the stub returned by the changed factory is the object built
* by the original factory, but deeply stubbed (see {@link #deepStub}).
* Alternatively, a custom stub may be given explicitly that is used instead.
*
* @param object {Object} Namespace to hold factory, e.g. qx.html.
* @param property {String} Property as string that functions as
* constructor, e.g. "Element".
* @param customStub {Object?} Stub to inject.
* @return {Object} Injected stub.
*/
injectStub: function(object, property, customStub) {
var stub = customStub || this.deepStub(new object[property]);
this.stub(object, property).returns(stub);
return stub;
},
/**
* Changes the given factory (e.g. a constructor) to make a mock of the
* object returned. The method itself returns this mock.
*
* By default, the object returned by the changed factory (that a mock is
* made of) is a deep copied, API-identical clone of the object built by the
* original factory. Alternatively, the object returned can be given
* explicitly.
*
* @param object {Object} Namespace to hold factory, e.g. qx.html.
* @param property {String} Property as string that functions as
* constructor, e.g. "Element".
* @param customObject {Object?} Object to inject.
* @return {Object} Mock of the object built.
*/
revealMock: function(object, property, customObject) {
var source = customObject ||
this.__deepClone(new object[property]);
this.stub(object, property).returns(source);
return this.mock(source);
},
/**
* Deep clone object by copying properties from prototype.
*
* @param obj {Object} Object to prepare (that is, clone).
* @return {Object} Prepared (deeply cloned) object.
*/
__deepClone: function(obj) {
var clone = {};
// Copy from prototype
for (var prop in obj) {
clone[prop] = obj[prop];
}
return clone;
},
/**
* Get the object’s own properties.
*
* @param object {Object} Object to analyze.
* @param targetClazz {Object} Class which marks the end of the chain.
* @return {Array} Array of the object’s own properties.
*/
__getOwnProperties: function(object, targetClazz) {
var clazz = object.constructor,
clazzes = [],
properties = [];
// Find classes in inheritance chain up to targetClazz
if (targetClazz) {
while(clazz.superclass) {
clazzes.push(clazz);
clazz = clazz.superclass;
if (clazz == targetClazz.superclass) {
break;
}
}
}
// Check if property is own in one of the classes in chain
for (var prop in object) {
if (clazzes.length) {
var found = clazzes.some(function(clazz) {
return clazz.prototype.hasOwnProperty(prop);
});
if (!found) {
continue;
}
}
properties.push(prop);
}
return properties;
},
/**
* Safely stub property.
*
* @param object {Object} Object to stub.
* @param prop {String} Property to stub.
*/
__stubProperty: function(object, prop) {
// Leave constructor and properties intact
if(prop === "constructor" || typeof object[prop] !== "function") {
return;
}
this.stub(object, prop);
}
}
});