I am writing an end-to-end test using Protractor for my Angular application. I can mock httpBackend for unit test but I want to actually call the server and get the JSON re
Protractor should be used for end-to-end testing of your full stack.
In this scenario the test typically exercises the angular application (filling form, pressing buttons) which will trigger the angular application to call to the REST server, which returns data, which your Angular application transforms in DOM changes, which then your end-to-end test asserts on.
This means that you probably want to start your application server (which hosts the angular application and is the REST backend, I suppose) before starting Protractor
How to do that is out of scope for Protractor.
The difficulty in this is typically how to setup your database, so that the e2e test knows what to expect as returns to your JSON services.
I'm working through this myself at the moment. The short answer I think is that you set up your application exactly the same as if you were manually testing it yourself - so Protractor is really just a robot user, it has no (well, almost no) access to the internals of your application.
So, if your application needs a web server (and most do), then you start up that web server, then have protractor connect to your application via the browser and exercise it.
For my case, I'm aiming to use grunt to call a task that does basic database setup before it starts running my protractor e2e tests - this should give me a known database state.
For an example of this, I've been writing a tutorial for using Rails 4 with AngularJS, the section on using protractor for e2e testing is not rails-specific and might be useful: http://technpol.wordpress.com/2013/11/16/5-end-to-end-testing/
Below is an example of how to automatically start and stop a separate node server only while the e2e tests run. A simple express mock server script is included as an example API.
protractor.conf.js
const {SpecReporter} = require('jasmine-spec-reporter');
const forever = require('forever-monitor');
const child = new (forever.Monitor)('index.js', {
max: 10,
silent: false,
args: ["--port", "3001"],
sourceDir: 'mock-server'
});
let startResolve;
let stopResolve;
const startPromise = new Promise((resolve) => startResolve = resolve);
const stopPromise = new Promise((resolve) => stopResolve = resolve);
child.on('start', function () {
console.info('Forever started mocks.');
startResolve();
});
child.on('restart', function () {
console.info('Forever restarting mocks for ' + child.times + ' time');
});
child.on('exit:code', function (code) {
if (code) {
console.info('Forever exit mocks with code ' + code);
} else {
console.info('Forever exited mocks.');
}
stopResolve();
});
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function () {
}
},
beforeLaunch: function () {
child.start();
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
return startPromise;
},
onPrepare() {
jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
},
onCleanUp() {
child.stop();
return stopPromise;
}
};
mock-server/index.js
// npm install --save express
// npm install --save body-parser
// npm install --save minimist
const express = require('express');
const bodyParser = require('body-parser');
const minimist = require('minimist');
const API_DELAY = 0;
const app = express();
app.use(bodyParser.json({limit: '50mb'}));
// Turn on CORS for browser testing.
app.use(function (req, res, next) {
let accessHeaderInReq = false;
if (req.headers.origin) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
accessHeaderInReq = true;
}
if (req.headers['access-control-request-method']) {
res.header('Access-Control-Allow-Methods', req.headers['access-control-request-method']);
accessHeaderInReq = true;
}
if (req.headers['access-control-request-headers']) {
res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
accessHeaderInReq = true;
}
if (accessHeaderInReq) {
res.header('Access-Control-Max-Age', 60 * 60 * 24 * 365);
}
// Intercept OPTIONS method for angular preflight checks.
if (accessHeaderInReq && req.method === 'OPTIONS') {
return res.sendStatus(200);
}
else {
next();
}
});
app.get('/api/foo', function (req, res, next) {
console.info('GET - returning foo', req.body);
setTimeout(() => {
res.json({
foo: "bar"
});
}, API_DELAY);
});
const argv = minimist(process.argv.slice(2));
const port = argv.port || 3000;
console.log("Starting express on port", port);
app.listen(port);
For continuous integration environments, you can install the mock server node_modules without changing directories like this:
npm --prefix ./mock-server install ./mock-server
We do this type of "hit the API directly and test the data in the response" integration testing in addition to our Protractor e2e testing. For the API-side testing, you don't need Protractor, because there's no need to fire up a browser just to send HTTP requests to a server.
Here's what we do:
describe()/it()/expect()
grammar from our Protractor environment (which is based on Jasmine). So rather than running protractor to run tests, you run jasmine, a la: jasmine --config=jasmine.json path/to/tests/*spec.js
Our spec files look something like this:
describe('API Tests written in Jasmine', function() {
beforeAll(() => authAsAdmin());
it('Should get a proposal object as auth\'d user', function() {
const httpOptions = {
uri: `/proposals/100`,
};
return requestWithAuth(httpOptions)
.then(res => {
const proposal = res.body.proposal;
// console.log(`Proposal ${proposal.id} title: ${proposal.title}`);
expect(proposal.id).toEqual(100);
expect(res.statusCode).toEqual(200);
expect(res.statusMessage).toBe('OK');
});
});
Our spec files depend on some global helper methods that we set up in a Jasmine helper file (part of the standard mechanics of how Jasmine works), like below:
const rp = require('request-promise');
...
// Declare our helper methods globally so they can be accessed anywhere in tests
global.requestWithAuth = requestWithAuth;
global.authAs = authAs;
global.authAsAdmin = () => authAs(ADMIN_USER);
global.catchErrorInLocation = (error, location) => {
throw new Error(`Error in ${location}\n ${error}`);
};
global.catchErrorInBeforeAll = (error) => catchErrorInLocation(error, 'beforeAll()');
function authAs(user) {
...
}
/**
* Combines a given set of options with the DEFAULT_HTTP_OPTIONS plus a session token
* and initiates an http request, returning a promise for the response.
* @param {Object} options properties matching request-promise API
* @param {string} token, optional session token. sessionToken used by default.
* @returns {Promise} request-promise response
*/
function requestWithAuth(options, token = sessionToken) {
Object.assign(options, { ...DEFAULT_HTTP_OPTIONS, ...options }); // Merge custom options with default options
options.headers['x-token'] = token; // Merge current session token into options
options.uri = `${BASE_URL}${options.uri}`; // Update the URI to include the correct base path
return rp(options);
}
I hope this helps.