What's a good way to reuse test code using Jasmine?
I'm using the Jasmine BDD Javascript library and really enjoying it. I have test code that I'd like to reuse (for example, testing multiple implementations of a 开发者_开发技巧base class or running the same tests in a slightly different context) and I'm not sure how to do it using Jasmine. I know that I could move code out of the jasmine functions and into reusable classes but I like the way the code reads interspersed with the Jasmine functions (describe, it) and I don't want to separate the specs from the test code unless I have to. Has anyone out there using Jasmine come across this issue and how have you handled it?
Here is an article by a guy at Pivotal Labs that goes into detail about how to wrap a describe call:
DRYing up Jasmine Specs with Shared Behavior
Snippet from the article that shows part of the wrapper function:
function sharedBehaviorForGameOf(context) {
describe("(shared)", function() {
var ball, game;
beforeEach(function() {
ball = context.ball;
game = context.game;
});
});
}
I'm not sure how @starmer's solution works. As I mentioned in the comment, when I use his code, context
is always undefined.
Instead what you have to do (as mentioned by @moefinley) is to pass in a reference to a constructor function instead. I've written a blog post that outlines this approach using an example. Here's the essence of it:
describe('service interface', function(){
function createInstance(){
return /* code to create a new service or pass in an existing reference */
}
executeSharedTests(createInstance);
});
function executeSharedTests(createInstanceFn){
describe('when adding a new menu entry', function(){
var subjectUnderTest;
beforeEach(function(){
//create an instance by invoking the constructor function
subjectUnderTest = createInstanceFn();
});
it('should allow to add new menu entries', function(){
/* assertion code here, verifying subjectUnderTest works properly */
});
});
}
There's a nice article on thoughbot's website: https://robots.thoughtbot.com/jasmine-and-shared-examples
Here's a brief sample:
appNamespace.jasmine.sharedExamples = {
"rectangle": function() {
it("has four sides", function() {
expect(this.subject.sides).toEqual(4);
});
},
};
And with some underscore functions to define itShouldBehaveLike
window.itShouldBehaveLike = function() {
var exampleName = _.first(arguments),
exampleArguments = _.select(_.rest(arguments), function(arg) { return !_.isFunction(arg); }),
innerBlock = _.detect(arguments, function(arg) { return _.isFunction(arg); }),
exampleGroup = appNamespace.jasmine.sharedExamples[exampleName];
if(exampleGroup) {
return describe(exampleName, function() {
exampleGroup.apply(this, exampleArguments);
if(innerBlock) { innerBlock(); }
});
} else {
return it("cannot find shared behavior: '" + exampleName + "'", function() {
expect(false).toEqual(true);
});
}
};
Let me summarize it with working example
describe('test', function () {
beforeEach(function () {
this.shared = 1;
});
it('should test shared', function () {
expect(this.shared).toBe(1);
});
testShared();
});
function testShared() {
it('should test in function', function() {
expect(this.shared).toBe(1);
});
}
The crucial parts here are this keyword to pass context and because of this we have to use "normal" functions (another crucial part).
For production code I would probably use normal function only in beforeEach
to pass/extract context but keep to use arrow-function in specs for brevity.
Passing context as parameter wouldn't work because normally we define context in beforeEach
block wich invoked after.
Having describe
section seems not important, but still welcome for better structure
This is similar to starmer's answer, but after working through it I found some differences to point out. The downside is that if the spec fails you just see 'should adhere to common saving specifications' in the Jasmine report. The stack trace is the only way to find where it failed.
// common specs to execute
self.executeCommonSpecifications = function (vm) {
// I found having the describe( wrapper here doesn't work
self.shouldCallTheDisplayModelsSaveMethod(vm);
}
self.shouldCallTheDisplaysSaveMethod = function (vm) {
expect(vm.save.calls.count()).toBe(1);
};
// spec add an it so that the beforeEach is called before calling this
beforeEach(function(){
// this gets called if wrapped in the it
vm.saveChanges();
}
it('should adhere to common saving specifications', function () {
executeSavingDisplaysCommonSpecifications(vm);
});
This is the approach I have taken, inspired by this article:
https://gist.github.com/traviskaufman/11131303
which is based on Jasmine own documentation:
http://jasmine.github.io/2.0/introduction.html#section-The_%3Ccode%3Ethis%3C/code%3E_keyword
By setting shared dependencies as properties of beforeEach
function prototype, you can extend beforeEach
to make this dependencies available via this
.
Example:
describe('A suite', function() {
// Shared setup for nested suites
beforeEach(function() {
// For the sake of simplicity this is just a string
// but it could be anything
this.sharedDependency = 'Some dependency';
});
describe('A nested suite', function() {
var dependency;
beforeEach(function() {
// This works!
dependency = this.sharedDependency;
});
it('Dependency should be defined', function() {
expect(dependency).toBeDefined();
});
});
describe('Check if string split method works', function() {
var splitToArray;
beforeEach(function() {
splitToArray = this.sharedDependency.split();
});
it('Some other test', function() { ... });
});
});
I know my example is kind of useless but it should serve its purpose as code example.
Of course this is just one of the many things you could do to achieve what you say, I'm sure that more complex design patterns may be applied on top or aside to this one.
Hope it helps!
Here is a simpler solution. Declare a variable function and use it, without using the this keyword or context:
describe("Test Suit", function ()
{
var TestCommonFunction = function(inputObjects)
{
//common code here or return objects and functions here etc
};
it("Should do x and y", function()
{
//Prepare someInputObjects
TestCommonFunction(someInputObjects);
//do the rest of the test or evaluation
});
});
You can also return an object with more functions and call the returned functions thereafter.
I wasn't hapy about having to instantiate a whole new Component for my Angular tests that were sharing logic on different function calls (e.g. a private function called from two separate public functions on the component). The TestBed intantiates the component and doing so by hand just to run shared tests seemed dirty to me. I fixed my issue by avoiding the component reference altogether and calling functions by their name in the shared testsuite:
describe('firstFunction', () => {
itShouldExecuteSharedFunction('firstFunction');
});
describe('secondFunction', () => {
itShouldExecuteSharedFunction('secondFunction');
});
function itShouldExecuteSharedFunction(fnName: string) {
describe('sharedFunction', () => {
beforeEach(() => {...});
afterEach(() => {...});
it('should do something', () => {
component[fnName]();
expect(...);
});
});
}
It was pointed out to me to wrap a describe call in a function that passes it a parameter.
精彩评论