Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

RSpec-inspired "sharedExamplesFor" and "itBehavesLike" #172

Closed
wants to merge 1 commit into from

2 participants

@alexeits

A way to re-use groups of specs and suites in different contexts.

Shared example groups are defined using "sharedExamplesFor" and then realized in other contexts with "itBehavesLike".

@infews
Owner

Thanks for the idea (which we've blogged about and discussed a lot on the list) and the work.

Since this technique can be used without introducing the concept of shared groups to the core code we're not going to pull this - Google groups is failing at the moment, but you can do a search in the mailing list for a how to.

@infews infews closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 7, 2012
  1. Shared example group: "sharedExamplesFor" and "itBehavesLike" similar…

    Alex Tsibulya authored
    … to RSpec 2
This page is out of date. Refresh to see the latest.
View
193 lib/jasmine-core/jasmine.js
@@ -197,12 +197,12 @@ jasmine.any = function(clazz) {
};
/**
- * Returns a matchable subset of a hash/JSON object. For use in expectations when you don't care about all of the
+ * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the
* attributes on the object.
*
* @example
* // don't care about any other attributes than foo.
- * expect(mySpy).toHaveBeenCalledWith(jasmine.hashContaining({foo: "bar"});
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"});
*
* @param sample {Object} sample
* @returns matchable object for the sample
@@ -602,6 +602,111 @@ var xdescribe = function(description, specDefinitions) {
};
if (isCommonJS) exports.xdescribe = xdescribe;
+/**
+ * Defines a shared example group (a shared suite) that can be called via 'itBehavesLike'.
+ *
+ * When declared, a shared example group stores the definition of all its specs in the Jasmine environment.
+ * It's only realized in the context of another suite, which provides any context that the shared group needs to run.
+ *
+ * @example
+ * describe("Shapes", function() {
+ *
+ * sharedExamplesFor("should have dimensions", function() {
+ * it("should have width", function(){
+ * expect(this.subject.width).not.toBeUndefined();
+ * });
+ * it("should have height", function(){
+ * expect(this.subject.height).not.toBeUndefined();
+ * });
+ * });
+ *
+ * describe("Rectangle", function(){
+ * beforeEach(function(){
+ * this.subject = new Rectangle(10, 20);
+ * });
+ * itBehavesLike("should have dimensions");
+ * });
+ *
+ * describe("Square", function(){
+ * beforeEach(function(){
+ * this.subject = new Square(10);
+ * });
+ * itBehavesLike("should have dimensions");
+ * });
+ * });
+ *
+ * @param {String} description A string describing the suite.
+ * 'itBehavesLike' uses this description to locate the suite.
+ * This name must be unique across the current suite and all ancestors.
+ * @param {Function} specDefinitions function that defines suite specs.
+ */
+var sharedExamplesFor = function(description, specDefinitions) {
+ return jasmine.getEnv().sharedExamplesFor(description, specDefinitions)
+};
+if (isCommonJS) exports.sharedExamplesFor = sharedExamplesFor;
+
+/**
+ * Realizes a shared example group previously defined via 'sharedExamplesFor' in the context of the caller suite.
+ *
+ * The shared examples group is first looked up in the current suite
+ * and then all the way up the ancestors tree until found.
+ *
+ * @example
+ * describe("Shapes and objects", function(){
+ *
+ * sharedExamplesFor("should have dimensions", function() {
+ * it("should have width", function(){
+ * expect(this.subject.width).not.toBeUndefined();
+ * });
+ * it("should have height", function(){
+ * expect(this.subject.height).not.toBeUndefined();
+ * });
+ * });
+ *
+ * describe("Square", function(){
+ * beforeEach(function(){
+ * this.subject = new Square(10);
+ * });
+ * itBehavesLike("should have dimensions");
+ * });
+ *
+ * describe("Cube", function(){
+ * beforeEach(function(){
+ * this.subject = new Cube(10);
+ * });
+ *
+ * it("should have six sides", function(){
+ * expect(this.subject.length).toEqual(6);
+ * });
+ *
+ * describe("side", function(){
+ * beforeEach(function(){
+ * this.subject = this.subject.sides[0];
+ * });
+ * itBehavesLike "should have dimensions";
+ * });
+ * });
+ * });
+ *
+ * @param description the description of the shared example group.
+ * It must exactly match the description passed to the corresponding 'sharedExamplesFor'.
+ */
+var itBehavesLike = function(description) {
+ return jasmine.getEnv().itBehavesLike(description);
+};
+if (isCommonJS) exports.itBehavesLike = itBehavesLike;
+
+/**
+ * Disables a shared example group previously defined via 'sharedExamplesFor'.
+ * Used to disable some invocations of shared examples temporarily during development.
+ *
+ * @param description the description of the shared example group.
+ * It must exactly match the description passed to the corresponding 'sharedExamplesFor'.
+ */
+var xitBehavesLike = function(description) {
+ return jasmine.getEnv().xitBehavesLike(description);
+};
+if (isCommonJS) exports.xitBehavesLike = xitBehavesLike;
// Provide the XMLHttpRequest class for IE 5.x-6.x:
jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() {
@@ -867,6 +972,53 @@ jasmine.Env.prototype.xit = function(desc, func) {
};
};
+jasmine.Env.prototype.sharedExamplesFor = function(description, specDefinitions) {
+ if (!description) {
+ throw new Error("Shared examples must have a description.");
+ }
+
+ var parentSuite = this.currentSuite;
+ if (!parentSuite) {
+ throw new Error("Shared examples must be defined within a suite.")
+ }
+
+ if (this.findSharedExampleGroup(description)) {
+ throw new Error("Shared examples for \"" + description + "\" already defined.");
+ }
+
+ parentSuite.addSharedExampleGroup(new jasmine.SharedExampleGroup(description, specDefinitions));
+ return parentSuite;
+};
+
+jasmine.Env.prototype.findSharedExampleGroup = function(description) {
+ var suite = this.currentSuite;
+ while (suite) {
+ var sharedContexts = suite.sharedExampleGroups();
+ for (var i = 0; i < sharedContexts.length; i++) {
+ if (sharedContexts[i].description === description) {
+ return sharedContexts[i];
+ }
+ }
+ suite = suite.parentSuite;
+ }
+};
+
+jasmine.Env.prototype.itBehavesLike = function(description) {
+ return this.insertSharedExampleGroup(description, this.describe);
+};
+
+jasmine.Env.prototype.insertSharedExampleGroup = function(description, suiteFn) {
+ var sharedContext = this.findSharedExampleGroup(description);
+ if (!sharedContext) {
+ throw new Error("Shared examples for \"" + description + "\" not found.");
+ }
+ return suiteFn.call(this, "it behaves like " + description, sharedContext.specDefinitions);
+};
+
+jasmine.Env.prototype.xitBehavesLike = function(description) {
+ return this.insertSharedExampleGroup(description, this.xdescribe);
+};
+
jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
return true;
@@ -929,12 +1081,12 @@ jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {
return a.getTime() == b.getTime();
}
- if (a instanceof jasmine.Matchers.Any) {
- return a.matches(b);
+ if (a.jasmineMatches) {
+ return a.jasmineMatches(b);
}
- if (b instanceof jasmine.Matchers.Any) {
- return b.matches(a);
+ if (b.jasmineMatches) {
+ return b.jasmineMatches(a);
}
if (a instanceof jasmine.Matchers.ObjectContaining) {
@@ -1476,7 +1628,7 @@ jasmine.Matchers.Any = function(expectedClass) {
this.expectedClass = expectedClass;
};
-jasmine.Matchers.Any.prototype.matches = function(other) {
+jasmine.Matchers.Any.prototype.jasmineMatches = function(other) {
if (this.expectedClass == String) {
return typeof other == 'string' || other instanceof String;
}
@@ -1496,7 +1648,7 @@ jasmine.Matchers.Any.prototype.matches = function(other) {
return other instanceof this.expectedClass;
};
-jasmine.Matchers.Any.prototype.toString = function() {
+jasmine.Matchers.Any.prototype.jasmineToString = function() {
return '<jasmine.any(' + this.expectedClass + ')>';
};
@@ -1504,7 +1656,7 @@ jasmine.Matchers.ObjectContaining = function (sample) {
this.sample = sample;
};
-jasmine.Matchers.ObjectContaining.prototype.matches = function(other, mismatchKeys, mismatchValues) {
+jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {
mismatchKeys = mismatchKeys || [];
mismatchValues = mismatchValues || [];
@@ -1526,8 +1678,8 @@ jasmine.Matchers.ObjectContaining.prototype.matches = function(other, mismatchKe
return (mismatchKeys.length === 0 && mismatchValues.length === 0);
};
-jasmine.Matchers.ObjectContaining.prototype.toString = function () {
- return "<jasmine.hashContaining(" + jasmine.pp(this.sample) + ")>";
+jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () {
+ return "<jasmine.objectContaining(" + jasmine.pp(this.sample) + ")>";
};
/**
* @constructor
@@ -1669,8 +1821,8 @@ jasmine.PrettyPrinter.prototype.format = function(value) {
this.emitScalar('null');
} else if (value === jasmine.getGlobal()) {
this.emitScalar('<global>');
- } else if (value instanceof jasmine.Matchers.Any) {
- this.emitScalar(value.toString());
+ } else if (value.jasmineToString) {
+ this.emitScalar(value.jasmineToString());
} else if (typeof value === 'string') {
this.emitString(value);
} else if (jasmine.isSpy(value)) {
@@ -1943,6 +2095,10 @@ jasmine.Runner.prototype.topLevelSuites = function() {
jasmine.Runner.prototype.results = function() {
return this.queue.results();
};
+jasmine.SharedExampleGroup = function(description, specDefinitions) {
+ this.description = description;
+ this.specDefinitions = specDefinitions;
+};
/**
* Internal representation of a Jasmine specification, or test.
*
@@ -2207,6 +2363,7 @@ jasmine.Suite = function(env, description, specDefinitions, parentSuite) {
self.children_ = [];
self.suites_ = [];
self.specs_ = [];
+ self.sharedExampleGroups_ = [];
};
jasmine.Suite.prototype.getFullName = function() {
@@ -2250,6 +2407,10 @@ jasmine.Suite.prototype.add = function(suiteOrSpec) {
this.queue.add(suiteOrSpec);
};
+jasmine.Suite.prototype.addSharedExampleGroup = function(sharedExampleGroup) {
+ this.sharedExampleGroups_.push(sharedExampleGroup);
+};
+
jasmine.Suite.prototype.specs = function() {
return this.specs_;
};
@@ -2258,6 +2419,10 @@ jasmine.Suite.prototype.suites = function() {
return this.suites_;
};
+jasmine.Suite.prototype.sharedExampleGroups = function() {
+ return this.sharedExampleGroups_;
+};
+
jasmine.Suite.prototype.children = function() {
return this.children_;
};
@@ -2524,5 +2689,5 @@ jasmine.version_= {
"major": 1,
"minor": 1,
"build": 0,
- "revision": 1299963843
+ "revision": 1325914180
};
View
53 spec/core/SpecRunningSpec.js
@@ -579,6 +579,43 @@ describe("jasmine spec running", function () {
expect(quux).toEqual(1);
});
+ it('should run shared suites', function () {
+
+ var invocations = [];
+
+ var suite = env.describe('top suite', function () {
+ env.beforeEach(function() {
+ this.context = 'In top suite';
+ });
+
+ env.sharedExamplesFor('shared suite', function() {
+ env.it('should run shared spec 1', function() {
+ invocations.push(this.context + " shared spec 1");
+ });
+ env.it('should run shared spec 2', function() {
+ invocations.push(this.context + " shared spec 2");
+ });
+ });
+
+ env.itBehavesLike('shared suite');
+
+ env.describe('nested suite', function() {
+ env.beforeEach(function() {
+ this.context = 'In nested suite';
+ });
+ env.itBehavesLike('shared suite');
+ });
+ });
+
+ expect(invocations.length).toEqual(0);
+ suite.execute();
+ expect(invocations.length).toEqual(4);
+ expect(invocations[0]).toEqual('In top suite shared spec 1');
+ expect(invocations[1]).toEqual('In top suite shared spec 2');
+ expect(invocations[2]).toEqual('In nested suite shared spec 1');
+ expect(invocations[3]).toEqual('In nested suite shared spec 2');
+ });
+
describe('#waitsFor should allow consecutive calls', function () {
var foo;
beforeEach(function () {
@@ -1179,6 +1216,22 @@ describe("jasmine spec running", function () {
expect(spy).not.toHaveBeenCalled();
});
+ it("shouldn't run disabled #itBehavesLike", function() {
+ var sharedSpecWasRun = false;
+ var suite = env.describe('suite', function () {
+ env.sharedExamplesFor('shared suite', function () {
+ env.it('should run shared spec 1', function () {
+ sharedSpecWasRun = true;
+ });
+ });
+
+ env.xitBehavesLike('shared suite');
+ });
+
+ suite.execute();
+ expect(sharedSpecWasRun).toEqual(false);
+ });
+
it('#explodes should throw an exception when it is called inside a spec', function() {
var exceptionMessage = false;
var anotherSuite = env.describe('Spec', function () {
View
59 spec/core/SuiteSpec.js
@@ -34,12 +34,28 @@ describe('Suite', function() {
this.expect(true).toEqual(true);
});
});
+ env.sharedExamplesFor('Examples 1', function () {
+ env.it('Spec 6', function () {
+ this.runs(function () {
+ this.expect(true).toEqual(true);
+ });
+ });
+ });
+ env.itBehavesLike('Examples 1');
});
env.it('Spec 4', function() {
this.runs(function () {
this.expect(true).toEqual(true);
});
});
+ env.sharedExamplesFor('Examples 2', function () {
+ env.it('Spec 5', function () {
+ this.runs(function () {
+ this.expect(true).toEqual(true)
+ });
+ });
+ });
+ env.itBehavesLike('Examples 2');
});
});
@@ -53,17 +69,56 @@ describe('Suite', function() {
it("#suites should return all immediate children that are suites.", function() {
var nestedSuites = suite.suites();
- expect(nestedSuites.length).toEqual(1);
+ expect(nestedSuites.length).toEqual(2);
expect(nestedSuites[0].description).toEqual('Suite 2');
+ expect(nestedSuites[1].description).toEqual('it behaves like Examples 2');
});
it("#children should return all immediate children including suites and specs.", function() {
var children = suite.children();
- expect(children.length).toEqual(4);
+ expect(children.length).toEqual(5);
expect(children[0].description).toEqual('Spec 1');
expect(children[1].description).toEqual('Spec 2');
expect(children[2].description).toEqual('Suite 2');
expect(children[3].description).toEqual('Spec 4');
+ expect(children[4].description).toEqual('it behaves like Examples 2');
+ });
+
+ it("#sharedContexts should return all immediate children that are shared contexts.", function() {
+ var sharedContexts = suite.sharedExampleGroups();
+ expect(sharedContexts.length).toEqual(1);
+ expect(sharedContexts[0].description).toEqual('Examples 2');
+ });
+ });
+
+ describe('SharedSuite', function() {
+ it("#sharedExamplesFor should throw an Error if the description is missing.", function() {
+ expect(function() {
+ env.sharedExamplesFor("", function() {});
+ }).toThrow(new Error("Shared examples must have a description."));
+ });
+
+ it("#sharedExamplesFor should throw an Error if there is no suite.", function() {
+ expect(function() {
+ env.sharedExamplesFor("Shared Suite", function() {});
+ }).toThrow(new Error("Shared examples must be defined within a suite."));
+ });
+
+ it("#sharedExamplesFor should throw an Error if the description is not unique.", function(){
+ env.describe("Suite", function() {
+ env.sharedExamplesFor("shared suite", function() {});
+ env.describe("Nested suite", function() {
+ expect(function() {
+ env.sharedExamplesFor("shared suite", function() {});
+ }).toThrow("Shared examples for \"shared suite\" already defined.");
+ });
+ });
+ });
+
+ it("#itBehavesLike should throw an Error if the shared suite is missing.", function() {
+ expect(function() {
+ env.itBehavesLike("missing suite");
+ }).toThrow(new Error("Shared examples for \"missing suite\" not found."));
});
});
View
47 src/core/Env.js
@@ -168,6 +168,53 @@ jasmine.Env.prototype.xit = function(desc, func) {
};
};
+jasmine.Env.prototype.sharedExamplesFor = function(description, specDefinitions) {
+ if (!description) {
+ throw new Error("Shared examples must have a description.");
+ }
+
+ var parentSuite = this.currentSuite;
+ if (!parentSuite) {
+ throw new Error("Shared examples must be defined within a suite.")
+ }
+
+ if (this.findSharedExampleGroup(description)) {
+ throw new Error("Shared examples for \"" + description + "\" already defined.");
+ }
+
+ parentSuite.addSharedExampleGroup(new jasmine.SharedExampleGroup(description, specDefinitions));
+ return parentSuite;
+};
+
+jasmine.Env.prototype.findSharedExampleGroup = function(description) {
+ var suite = this.currentSuite;
+ while (suite) {
+ var sharedContexts = suite.sharedExampleGroups();
+ for (var i = 0; i < sharedContexts.length; i++) {
+ if (sharedContexts[i].description === description) {
+ return sharedContexts[i];
+ }
+ }
+ suite = suite.parentSuite;
+ }
+};
+
+jasmine.Env.prototype.itBehavesLike = function(description) {
+ return this.insertSharedExampleGroup(description, this.describe);
+};
+
+jasmine.Env.prototype.insertSharedExampleGroup = function(description, suiteFn) {
+ var sharedContext = this.findSharedExampleGroup(description);
+ if (!sharedContext) {
+ throw new Error("Shared examples for \"" + description + "\" not found.");
+ }
+ return suiteFn.call(this, "it behaves like " + description, sharedContext.specDefinitions);
+};
+
+jasmine.Env.prototype.xitBehavesLike = function(description) {
+ return this.insertSharedExampleGroup(description, this.xdescribe);
+};
+
jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
return true;
View
4 src/core/SharedExampleGroup.js
@@ -0,0 +1,4 @@
+jasmine.SharedExampleGroup = function(description, specDefinitions) {
+ this.description = description;
+ this.specDefinitions = specDefinitions;
+};
View
9 src/core/Suite.js
@@ -19,6 +19,7 @@ jasmine.Suite = function(env, description, specDefinitions, parentSuite) {
self.children_ = [];
self.suites_ = [];
self.specs_ = [];
+ self.sharedExampleGroups_ = [];
};
jasmine.Suite.prototype.getFullName = function() {
@@ -62,6 +63,10 @@ jasmine.Suite.prototype.add = function(suiteOrSpec) {
this.queue.add(suiteOrSpec);
};
+jasmine.Suite.prototype.addSharedExampleGroup = function(sharedExampleGroup) {
+ this.sharedExampleGroups_.push(sharedExampleGroup);
+};
+
jasmine.Suite.prototype.specs = function() {
return this.specs_;
};
@@ -70,6 +75,10 @@ jasmine.Suite.prototype.suites = function() {
return this.suites_;
};
+jasmine.Suite.prototype.sharedExampleGroups = function() {
+ return this.sharedExampleGroups_;
+};
+
jasmine.Suite.prototype.children = function() {
return this.children_;
};
View
105 src/core/base.js
@@ -602,6 +602,111 @@ var xdescribe = function(description, specDefinitions) {
};
if (isCommonJS) exports.xdescribe = xdescribe;
+/**
+ * Defines a shared example group (a shared suite) that can be called via 'itBehavesLike'.
+ *
+ * When declared, a shared example group stores the definition of all its specs in the Jasmine environment.
+ * It's only realized in the context of another suite, which provides any context that the shared group needs to run.
+ *
+ * @example
+ * describe("Shapes", function() {
+ *
+ * sharedExamplesFor("should have dimensions", function() {
+ * it("should have width", function(){
+ * expect(this.subject.width).not.toBeUndefined();
+ * });
+ * it("should have height", function(){
+ * expect(this.subject.height).not.toBeUndefined();
+ * });
+ * });
+ *
+ * describe("Rectangle", function(){
+ * beforeEach(function(){
+ * this.subject = new Rectangle(10, 20);
+ * });
+ * itBehavesLike("should have dimensions");
+ * });
+ *
+ * describe("Square", function(){
+ * beforeEach(function(){
+ * this.subject = new Square(10);
+ * });
+ * itBehavesLike("should have dimensions");
+ * });
+ * });
+ *
+ * @param {String} description A string describing the suite.
+ * 'itBehavesLike' uses this description to locate the suite.
+ * This name must be unique across the current suite and all ancestors.
+ * @param {Function} specDefinitions function that defines suite specs.
+ */
+var sharedExamplesFor = function(description, specDefinitions) {
+ return jasmine.getEnv().sharedExamplesFor(description, specDefinitions)
+};
+if (isCommonJS) exports.sharedExamplesFor = sharedExamplesFor;
+
+/**
+ * Realizes a shared example group previously defined via 'sharedExamplesFor' in the context of the caller suite.
+ *
+ * The shared examples group is first looked up in the current suite
+ * and then all the way up the ancestors tree until found.
+ *
+ * @example
+ * describe("Shapes and objects", function(){
+ *
+ * sharedExamplesFor("should have dimensions", function() {
+ * it("should have width", function(){
+ * expect(this.subject.width).not.toBeUndefined();
+ * });
+ * it("should have height", function(){
+ * expect(this.subject.height).not.toBeUndefined();
+ * });
+ * });
+ *
+ * describe("Square", function(){
+ * beforeEach(function(){
+ * this.subject = new Square(10);
+ * });
+ * itBehavesLike("should have dimensions");
+ * });
+ *
+ * describe("Cube", function(){
+ * beforeEach(function(){
+ * this.subject = new Cube(10);
+ * });
+ *
+ * it("should have six sides", function(){
+ * expect(this.subject.length).toEqual(6);
+ * });
+ *
+ * describe("side", function(){
+ * beforeEach(function(){
+ * this.subject = this.subject.sides[0];
+ * });
+ * itBehavesLike "should have dimensions";
+ * });
+ * });
+ * });
+ *
+ * @param description the description of the shared example group.
+ * It must exactly match the description passed to the corresponding 'sharedExamplesFor'.
+ */
+var itBehavesLike = function(description) {
+ return jasmine.getEnv().itBehavesLike(description);
+};
+if (isCommonJS) exports.itBehavesLike = itBehavesLike;
+
+/**
+ * Disables a shared example group previously defined via 'sharedExamplesFor'.
+ * Used to disable some invocations of shared examples temporarily during development.
+ *
+ * @param description the description of the shared example group.
+ * It must exactly match the description passed to the corresponding 'sharedExamplesFor'.
+ */
+var xitBehavesLike = function(description) {
+ return jasmine.getEnv().xitBehavesLike(description);
+};
+if (isCommonJS) exports.xitBehavesLike = xitBehavesLike;
// Provide the XMLHttpRequest class for IE 5.x-6.x:
jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() {
View
2  src/version.js
@@ -2,5 +2,5 @@ jasmine.version_= {
"major": 1,
"minor": 1,
"build": 0,
- "revision": 1320442951
+ "revision": 1325914180
};
Something went wrong with that request. Please try again.