This started as an exercise to solve a huge refactor issue I have and turned into an even larger architectural refactoring. The problem, some e2e and functional tests have grown out of control and being based on the user's POV the assertions are repetitive with subtle variations. The immediate solution is obvious, update all the tests and move on. However, I like being a lazy engineer (thanks to Jem Young ) and really don't want to go through this process again.
The solution I came up with is abstracting the tests to a module. Note: the SupportFunctions module is just a group of a methods that handle login and so forth.
Starting spec files:
/* base spec file 1 */
'use strict';
const SF = require( '../path/SupportFunctions' );
describe( 'landing page', () => {
beforeAll( () => {
SF.login( validUser );
} );
describe( 'page header', () => {
it( 'displays the header', () => {
expect( element( by.css( '.header' ) ).isDisplayed() ).toBe( true );
} );
it( 'displays the menu bar', () => {
expect( element( by.css( '.menu-bar' ) ).isDisplayed() ).toBe( true );
} );
it( 'hides the error page', () => {
expect( element( by.css( '.error-page' ) ).isDisplayed() ).not.toBe( true );
} );
/** remaining test here **/
} );
} );
/* base spec file 2 */
'use strict';
const SF = require( '../path/SupportFunctions' );
describe( 'landing page', () => {
beforeAll( () => {
SF.login( invalidUser );
} );
describe( 'page header', () => {
it( 'displays the header', () => {
expect( element( by.css( '.header' ) ).isDisplayed() ).not.toBe( true );
} );
it( 'displays the menu bar', () => {
expect( element( by.css( '.menu-bar' ) ).isDisplayed() ).not.toBe( true );
} );
it( 'displays the error page', () => {
expect( element( by.css( '.error-page' ) ).isDisplayed() ).toBe( true );
} );
/** remaining test here **/
} );
} );
As you can see the workflows are the same, but the assertions have different expectations. Here are the two ways I will incorporate modules to simplify maintenance. The first is to abstract the it() methods.
/* it() abstraction module */
'use strict';
let ItModularJasmine = ( () => {
function ItModularJasmine() {}
ItModularJasmine.prototype = {
headerAssertion: function( isTrue ) {
return it( 'displays the header', () => {
expect( element( by.css( '.header' ) ).isDisplayed() ).toBe( isTrue );
} );
},
menuBarAssertion: function( isTrue ) {
return it( 'displays the menu bar', () => {
expect( element( by.css( '.menu-bar' ) ).isDisplayed() ).toBe( isTrue );
} );
},
errorPageAssertion: function( isTrue ) {
return it( 'displays the error page', () => {
expect( element( by.css( '.error-page' ) ).isDisplayed() ).toBe( isTrue );
} );
}
}
return ItModularJasmine;
} )();
module.exports = new ItModularJasmine();
Now with our test abstraction module in place our specs files become a lot clearer and easier to maintain.
/* it() modular file 1 */
'use strict';
const MJ = require( '../path/ItModuleJasmine.module' ),
SF = require( '../path/SupportFunctions' );
describe( 'landing page', () => {
beforeAll( () => {
SF.login( validUser );
} );
describe( 'page header', () => {
MJ.headerAssertion( true );
MJ.menuBarAssertion( true );
MJ.errorPageAssertion( false );
} );
} );
/* it() modular file 2 */
'use strict';
const MJ = require( '../path/ItModuleJasmine.module' ),
SF = require( '../path/SupportFunctions' );
describe( 'landing page', () => {
beforeAll( () => {
SF.login( invalidUser );
} );
describe( 'page header', () => {
MJ.headerAssertion( false );
MJ.menuBarAssertion( false );
MJ.errorPageAssertion( true );
} );
} );
It() blocks are not the only thing that can be abstracted to a module. A whole describe() block can be abstracted as well. Which looks like this:
/* describe() module abstraction */
'use strict';
let DescribeModule = ( () => {
function DescribeModule {}
DescribeModule.prototype = {
pageHeaderAssertions: function( isHeader, isMenuBar, isErrorPage ) {
return describe( 'page header', () => {
it( 'displays the header', () => {
expect( element( by.css( '.header' ) ).isDisplayed() ).toBe( isHeader );
} );
it( 'displays the menu bar', () => {
expect( element( by.css( '.menu-bar' ) ).isDisplayed() ).toBe( isMenuBar );
} );
it( 'displays the error page', () => {
expect( element( by.css( '.error-page' ) ).isDisplayed() ).toBe( isErrorPage );
} );
} );
}
}
return DescribeModule;
} )();
module.exports = new DescribeModule();
Now the spec files are even clearer and shorter.
/* describe modular file 1 */
'use strict';
const MJ = require( '../path/DescribeModule' ),
SF = require( '../path/SupportFunctions' );
describe( 'landing page', () => {
beforeAll( () => {
SF.login( validUser );
} );
MJ.pageHeaderAssertions( true, true, false );
} );
/* describe module file 2*/
'use strict';
const MJ = require( '../path/DescribeModule' ),
SF = require( '../path/SupportFunctions' );
describe( 'landing page', () => {
beforeAll( () => {
SF.login( validUser );
} );
MJ.pageHeaderAssertions( false, false, true );
} );
As with anything in this modular pattern you can mix and match. The follow is the final refactoring of the describe module from above.
/* structure from before */
---
MixedModule.prototype = {
pageHeaderAssertions: function( isHeader, isMenuBar, isErrorPage ) {
return describe( 'page header', () => {
this.headerAssertion( isHeader );
this.menuBarAssertion( isMenuBar );
this.errorPageAssertion( isErrorPage );
} );
},
headerAssertion: function( isTrue ) {
return it( 'displays the header', () => {
expect( element( by.css( '.header' ) ).isDisplayed() ).toBe( isTrue );
} );
},
menuBarAssertion: function( isTrue ) {
return it( 'displays the menu bar', () => {
expect( element( by.css( '.menu-bar' ) ).isDisplayed() ).toBe( isTrue );
} );
},
errorPageAssertion: function( isTrue ) {
return it( 'displays the error page', () => {
expect( element( by.css( '.error-page' ) ).isDisplayed() ).toBe( isTrue );
} );
}
}
---
Update
Now that I've been working through reorg of my code here are some things I've done to make life easier and some issues with this type of structure.
And a couple templates it became clear that I needed a container file for all the require statements, other the top of the spec file would look like this:
'use strict';
const template1 = require( '../path/template1' ),
template2 = require( '../path/template2' );
/* and so forth */
The container is straight forward to setup;
module.exports = function() {
return {
template1: require( '../path/template1' ),
template2: require( '../path/template2' )
/* and so forth */
};
};
In the spec the usage looks like this:
'use strict';
const templates = require( '../path/container' );
templates().template1.method();
templates().template2.method();
Early Pitfall
In testing this concept out and trying various implementation mechanics I used console.log() for debugging (don't roll your eyes you do it too) and I had a couple of variables like this:
let input = input || 0;
Because of closure the variable wasn't a reliable value inside the methods that used it.
Top comments (2)
base spec file 1
what that user should see but just seeing true, true, false would mean I'd have to dig further to know what the app does. Not an issue if personnel stays put, but on-boarding a new QA when everything is abstracted away makes it harder to show what the app actually should do. Mainly thinking of it like the Protractor Style Guide's "Avoid using expect() in page objects" rulePassing "isTrue" allows the option change the assertion values. In practice all my assertion values are hardcoded to either true or false, since a case hasn't risen yet where this is needed.
As with all abstraction, layer rules and naming conventions will help keep things clear. While in theory having a single spec run doesEverythingWork() is feasible, everyone would have a hard time understanding what "everything" is and navigating the stack trace should anything break, would take a while and probably a lot of grep and sed filters.
The general folder structure I'm working with is:
While the templates use the same modular pattern as page objects they should definitely be separate files.