19

Just got started with Protractor for E2E testing and I am having a bit of trouble with the test case structure.

Not sure if can I divide my tests into separate specs and then call them from another or how can I make nice helper functions to handle this.

I am finding elements by a repeater and then I would like to make tests for each of the operation for each of the element in the repeater. Sort of like this:

describe('tasty', function () {
    'use strict';
    var ptor;

    beforeEach(function () {
        ptor = protractor.getInstance();
        ptor.get('http://localhost:8000/');
    });

    it('Should sample three tasty fruits of every kind on my shopping list.', function () {
        ptor.findElement(protractor.By.className('fruitstore')).click();
        var fruitshelves = ptor.findElements(protractor.By.repeater('fruit in fruits').column('header'));

        fruitshelves.then(function(arr) {
            for (var i=0;i<arr.length; i++) { 
                // Pick up three fruits of this kind from the shelf and put in shopping cart
                // Should be listed on my shopping list 
                // Open the wallet
                // Should have money
                // Pay for the fruits and put it in your shopping bag
                // Should be able to complete the transaction

                // For each one of the fruits in your shopping bag
                // Take a bite
                // Should be tasty
            }
        });
    });
});
nwinkler
  • 52,665
  • 21
  • 154
  • 168
Annie
  • 193
  • 1
  • 1
  • 4

4 Answers4

14

Based on the @langliman answer, I've managed to achieve the desired behaviour.

Note login.spec.js and Login.page.js should be located in the same folder.

Login.page.js file:

var LoginPage = function (ptor) {
    //following PageObject pattern define the functions here.
}

module.exports.getLoginPage = function (ptor) {
    return new LoginPage(ptor);
};

login.spec.js file:

(function () {
    'use strict';

  describe('login page', function () {

        var ptor = protractor.getInstance();
        var loginPageBuilder = require('./Login.page.js');
        var loginPage = loginPageBuilder.getLoginPage(ptor);

        it('should login as admin', function () {
            loginPage.visit();
            loginPage.enterUsername('user');
            loginPage.enterPassword('password');
            loginPage.login();
        });
  });

}());
WeMakeSoftware
  • 9,039
  • 5
  • 34
  • 52
11

I came to this question looking for a way to have helper functions shared between spec files in Protractor. In case others are looking for the same, turns out since Protractor is just running in Node, all you need to do is var helpers = require('./your-helper-file').

iangilman
  • 2,144
  • 1
  • 13
  • 16
  • 1
    hmm.. could you elaborate on that? I've started to use protractor, but I have almost no experience in Node. Do I have to configure smth? – WeMakeSoftware May 14 '14 at 06:50
  • @Funtik you need to add the methods inside the "your-helper-file.js" like in this answer: http://stackoverflow.com/a/5311377/3111930 *Edit: ah okay you answered yourself :-) – Sebastian Aug 06 '14 at 09:38
7

In case you want shared setup and before/after functions as well as helper methods, one solution is to require the tests from your spec helper instead of requiring your spec helper from the tests.

conf.js

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['e2e/spec.js']
}

e2e/spec.js

var chai = require('chai'),
    homepage = require('./homepage.js'),
    signin = require('./signin.js');

chai.should()
browser.baseUrl = 'http://localhost:3000'

homepage.test()
signin.test()

e2e/homepage.js

exports.test = function() {
  describe('homepage', function() {
    it('should have the right title', function() {
      browser.get('/')

      browser.getTitle().then(function(title){
        title.should.eq('Home')
      })
    });
  });
}

e2e/signin.js

exports.test = function() {
  describe('signin', function() {
    it('should have the right title', function() {
      browser.get('/signin')

      browser.getTitle().then(function(title){
        title.should.eq('Sign in')
      })
    });
  });
}
Neil Sarkar
  • 6,834
  • 8
  • 32
  • 30
2

I'm looking at the same thing myself, and to some extent I had hoped that you would have an answer for me on this question. :-)

Having said that, it appears that protractor is new enough that nobody really knows the answer, and I guess that makes my answer as good as the next persons.

Firstly, I'm using the page object notation that is described on the protractor getting started page, towards the bottom: https://github.com/angular/protractor/blob/master/docs/getting-started.md

This gives an element of modularity, my view here is that I end up with a set of classes, one per page, that abstract away some of the detail. So, for example, I might have a "foo" class, which includes in it abstractions like "foo.get" and "foo.validate(id, name, otherData)". This would be a way to pull out repeated code.

The bit that I haven't worked out is how to create a library of modules and then assemble those into a single set of scenarios. I have a few thoughts though:

  1. The underlying problem is the ability to include javascript files in each other - which really doesn't exist as a capability. There are third party libraries, which I'd prefer not to use, and I haven't seen a way to use Angular's module capability to do this.
  2. End 2 end testing can be very dependent on the order of the tests. So one test may create data, another test may then use that data. As an example, if you want a test that logs people on, you may need a test that registers people first. You probably don't want to put registration on the front of every test that you run. As such, you probably need a lot of control over the order of your test scenarios anyway
  3. As such, one option is to just put everything in one really big file. Which goes against everything we all learned in school, but I haven't really come up with a reason that wouldn't work. Then you can write functions and abstractions to your hearts content.
  4. If you follow that to the next stage, another option is to write a series of javascript files with strict naming conventions, then use grunt to concatenate them for you before executing them. So, for example:
    • A set of files named xxxx.page.scenario.js, which contain the "page object" definitions - basically helper methods for each page
    • A set of files named xxxx.functions.scenario.js, which contain common components of your scenarios - so maybe you have a register and logon set of actions, and you make that into a library function
    • A set of files named nnxx.scenarios.scenario.js, which contain the actual scripts themselves. These are numbered at the start (the nn), so we can concatenate them in a reliable sequence and thereby control which order our scripts run

I'm not yet saying this is a good idea, just that it at least superficially looks like it could work, and would give the desired result. My main concern is that it feels fragile - so as the test suite grows in size it would perhaps become very difficult to maintain. Perhaps another way to do this would be, instead of numbering the scenarios, to instead define them as dependencies, and have something that makes sure that any given script runs after any script it declares itself to be dependent on. That would maybe allow for subsetting of the scripts as well - so you could say "run the bar script" and the framework would know that the bar script needs the foo script run first, and maybe the login script. But it's OK to leave all the other scripts out.

EDIT: I see astrolabe as potentially a good answer here, it looks like it explicitly allows you to modularise your tests. https://github.com/stuplum/astrolabe. I've just completed a proof of concept with it, and it seems to do everything I might hope. The code for it ends up something like:

clubs.part.scenario.js:

/**
 * Partial for the page objects associated with clubs
 */
var Page = require('astrolabe').Page;

module.exports = Page.create({
  url: { value: 'UI/index.html#clubs' },
  title: { get: function() { return this.findElement(this.by.id('title')); } },
  description: { get: function() { return this.findElement(this.by.id('description')); } },
  clubTableElement: { value: function(rowNum, columnBinding) { 
    return this.findElement(this.by.repeater('club in clubs').row(rowNum).column(columnBinding)); } }
  }
);

clubs.scenario.js:

/**
 * End to end tests for the club functionality
 */

var homePage = require('../home/home.part.scenario.js');
var clubsPage = require('./clubs.part.scenario.js');

describe( 'Navigate to club list page', function() {
  it ( 'should allow navigation to the club list page', function() {

    homePage.go();
    expect(homePage.clubsLink.getText()).toEqual('Clubs');

    homePage.clubsLink.click();

    expect(clubsPage.title.getText()).toEqual('Club functions');
    expect(clubsPage.description.getText()).toEqual('Soon this will show a list of all the clubs, based on information from the server');
    expect(clubsPage.clubTableElement(0, 'name').getText()).toEqual('First club');    
    expect(clubsPage.clubTableElement(0, 'contact_officer').getText()).toEqual('A Person');    
    expect(clubsPage.clubTableElement(1, 'name').getText()).toEqual('Second club');    
    expect(clubsPage.clubTableElement(1, 'contact_officer').getText()).toEqual('J Jones');    
  }); 

  it ( 'should allow us to go directly to the club list page', function() {
    clubsPage.go();

    expect(clubsPage.title.getText()).toEqual('Club functions');
    expect(clubsPage.description.getText()).toEqual('Soon this will show a list of all the clubs, based on information from the server');
    expect(clubsPage.clubTableElement(0, 'name').getText()).toEqual('First club');    
    expect(clubsPage.clubTableElement(0, 'contact_officer').getText()).toEqual('A Person');    
    expect(clubsPage.clubTableElement(1, 'name').getText()).toEqual('Second club');    
    expect(clubsPage.clubTableElement(1, 'contact_officer').getText()).toEqual('J Jones');    
  }); 
});

I'm pretty happy with this structure, it doesn't do everything but it does most things. The sample code I've provided is from the tutorial that I've been working on for a while with angularjs, which I'm updating for e2e testing and Rails 4 at the moment, if you want the context that goes with that: http://technpol.wordpress.com/2013/11/16/5-end-to-end-testing/

PaulL
  • 6,650
  • 3
  • 35
  • 39
  • Further to this, I see this: https://github.com/dmitrybelyakov/protractor/blob/Feature/PageObject/docs/page-object.md, which provides a level of abstraction and allows sharing across test cases (I think). And also astrolabe, which seems to explicitly allow sharing of content: https://github.com/stuplum/astrolabe – PaulL Nov 16 '13 at 23:17
  • Paul, thanks for a really good answer! Had a look at astrolabe today and it looks like something that I would like to start using. As for now I will stick to option 3 as I am still in the progress of learning Protractor and JavaScript :-) – Annie Nov 18 '13 at 22:13
  • I've made further progress on astrolabe since I wrote that - I think I'm at a similar point to you in the life cycle. I've got it running in my real app (as opposed to the tutorial) and it's working very well. And, to be honest, it was really really easy to install. I'd give it a go, rather than having to refactor later. – PaulL Nov 19 '13 at 01:19
  • Yes, using the PageObject pattern works really well. – gontard Feb 13 '14 at 09:22
  • I disagree on the point that the tests could be dependent. In my opinion, they must be independent. You could share logic between tests using PageObjects, use database dump to restore the database at specific state. – gontard Feb 13 '14 at 09:26