1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 17:33:14 +01:00

Merge pull request #136 from ascribe/AD-1528-set-up-integration-testing-on-onion

Set up integration testing on onion
This commit is contained in:
Tim Daubenschütz 2016-01-26 10:47:54 +01:00
commit 533a9efa8f
10 changed files with 451 additions and 5 deletions

3
.env-template Normal file
View File

@ -0,0 +1,3 @@
SAUCE_USERNAME=ascribe
SAUCE_ACCESS_KEY=
SAUCE_DEFAULT_URL=

View File

@ -2,7 +2,7 @@
"parser": "babel-eslint", "parser": "babel-eslint",
"env": { "env": {
"browser": true, "browser": true,
"es6": true "es6": true,
}, },
"rules": { "rules": {
"new-cap": [2, {newIsCap: true, capIsNew: false}], "new-cap": [2, {newIsCap: true, capIsNew: false}],

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ node_modules/*
build build
.DS_Store .DS_Store
.env

View File

@ -8,9 +8,12 @@
}, },
"scripts": { "scripts": {
"lint": "eslint ./js", "lint": "eslint ./js",
"preinstall": "export SAUCE_CONNECT_DOWNLOAD_ON_INSTALL=true",
"postinstall": "npm run build", "postinstall": "npm run build",
"build": "gulp build --production", "build": "gulp build --production",
"start": "node server.js" "start": "node server.js",
"test": "mocha",
"tunnel": "node test/tunnel.js"
}, },
"browser": { "browser": {
"fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js" "fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js"
@ -35,8 +38,14 @@
"devDependencies": { "devDependencies": {
"babel-eslint": "^3.1.11", "babel-eslint": "^3.1.11",
"babel-jest": "^5.2.0", "babel-jest": "^5.2.0",
"gulp-sass": "^2.1.1", "chai": "^3.4.1",
"jest-cli": "^0.4.0" "chai-as-promised": "^5.1.0",
"colors": "^1.1.2",
"dotenv": "^1.2.0",
"jest-cli": "^0.4.0",
"mocha": "^2.3.4",
"sauce-connect-launcher": "^0.13.0",
"wd": "^0.4.0"
}, },
"dependencies": { "dependencies": {
"alt": "^0.16.5", "alt": "^0.16.5",
@ -62,7 +71,7 @@
"gulp-if": "^1.2.5", "gulp-if": "^1.2.5",
"gulp-minify-css": "^1.1.6", "gulp-minify-css": "^1.1.6",
"gulp-notify": "^2.2.0", "gulp-notify": "^2.2.0",
"gulp-sass": "^2.0.1", "gulp-sass": "^2.1.1",
"gulp-sourcemaps": "^1.5.2", "gulp-sourcemaps": "^1.5.2",
"gulp-template": "~3.0.0", "gulp-template": "~3.0.0",
"gulp-uglify": "^1.2.0", "gulp-uglify": "^1.2.0",

36
test/.eslintrc Normal file
View File

@ -0,0 +1,36 @@
{
"parser": "babel-eslint",
"env": {
"mocha": true,
"node": true
},
"rules": {
"new-cap": [2, {newIsCap: true, capIsNew: false}],
"quotes": [2, "single"],
"eol-last": [0],
"no-mixed-requires": [0],
"no-underscore-dangle": [0],
"global-strict": [2, "always"],
"no-trailing-spaces": [2, { skipBlankLines: true }],
"no-console": 0,
"camelcase": [2, {"properties": "never"}],
},
"globals": {},
"plugins": [],
"ecmaFeatures": {
"modules": 1,
"arrowFunctions",
"classes": 1,
"blockBindings": 1,
"defaultParams": 1,
"destructuring": 1,
"objectLiteralComputedProperties": 1,
"objectLiteralDuplicateProperties": 0,
"objectLiteralShorthandMethods": 1,
"objectLiteralShorthandProperties": 1,
"restParams": 1,
"spread": 1,
"superInFunctions": 1,
"templateStrings": 1
}
}

256
test/README.md Normal file
View File

@ -0,0 +1,256 @@
# TL;DR
Copy the contents of `.env-template` to `.env` and [fill up the missing keys with
information from your SauceLabs account](#how-to-set-up-your-env-config-file).
```bash
$ npm install
$ npm run tunnel
$ npm test && git commit
```
# TODO
* Use gulp to parallelize mocha invocations with different browsers
* Figure out good system for changing subdomain through test scripts
# Welcome to our test suite, let me be your guide
Dear reader, first of all thanks for taking your time reading this document.
The purpose of this document is to give you an overview on what we want to test
and how we are doing it.
# How it works (bird's-eye view)
You will notice that the setup is a bit convoluted. This section will explain
you why. Testing single functions in JavaScript is not that hard (if you don't
need to interact with the DOM), and can be easily achieved using frameworks
like [Mocha](https://mochajs.org/). Integration and cross browser testing is,
on the other side, a huge PITA. Moreover, "browser testing" includes also
"mobile browser testing". On the top of that the same browser (type and
version) can behave in a different way on different operating systems.
To achieve that you can have your own cluster of machines with different
operating systems and browsers or, if you don't want to spend the rest of your
life configuring an average of 100 browsers for each different operating
system, you can pay someone else to do that. Check out [this
article](https://saucelabs.com/selenium/selenium-grid) if you want to know why
using Selenium Grid is better than a DIY approach.
We decided to use [saucelabs](https://saucelabs.com/) cloud (they support [over
700 combinations](https://saucelabs.com/platforms/) of operating systems and
browsers) to run our tests.
## Components and tools
Right now we are just running the test locally, so no Continuous Integration™.
The components involved are:
- **[Selenium WebDriver](https://www.npmjs.com/package/wd)**: it's a library
that can control a browser. You can use the **WebDriver** to load new URLs,
click around, fill out forms, submit forms etc. It's basically a way to
control remotely a browser. The protocol (language agnostic) is called
[JsonWire](https://code.google.com/p/selenium/wiki/JsonWireProtocol), `wd`
wraps it and gives you a nice
[API](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md)
you can use in JavaScript. There are other implementations in Python, PHP,
Java, etc. Also, a **WebDriver** can be initialized with a list of [desired
capabilities](https://code.google.com/p/selenium/wiki/DesiredCapabilities)
describing which features (like the platform, browser name and version) you
want to use to run your tests.
- **[Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2)**: it's
the controller for the cluster of machines/devices that can run browsers.
Selenium Grid is able to scale by distributing tests on several machines,
manage multiple environments from a central point, making it easy to run the
tests against a vast combination of browsers / OS, minimize the maintenance
time for the grid by allowing you to implement custom hooks to leverage
virtual infrastructure for instance.
- **[Saucelabs](https://saucelabs.com/)**: a private company providing a
cluster to run your tests on over 700 combinations of browsers/operating
systems. (They do other things, check out their websites).
- **[SauceConnect](https://wiki.saucelabs.com/display/DOCS/Setting+Up+Sauce+Connect)**:
is a Java software by Saucelabs to connect to your `localhost` to test the
application. There is also a Node.js wrapper
[sauce-connect-launcher](https://www.npmjs.com/package/sauce-connect-launcher),
so you can use it programmatically within your code for tests. Please note
that this module is just a wrapper around the actual software. Running `npm
install` should install the additional Java software as well.
On the JavaScript side, we use:
- [Mocha](https://mochajs.org/): a test framework running on Node.js.
- [chai](http://chaijs.com/): a BDD/TDD assertion library for node that can be
paired with any javascript testing framework.
- [chaiAsPromised](https://github.com/domenic/chai-as-promised/): an extension
for Chai with a fluent language for asserting facts about promises. The
extension is actually quite cool, we can do assertions on promises without
writing callbacks but just chaining operators. Check out their `README` on
GitHub to see an example.
- [dotenv](https://github.com/motdotla/dotenv): a super nice package to load
environment variables from `.env` into `process.env`.
## How to set up your `.env` config file
In the root of this repository there is a file called `.env-template`. Create a
copy and call it `.env`. This file will store some values we need to connect to
Saucelabs.
There are two values to be set:
- `SAUCE_ACCESS_KEY`
- `SAUCE_USERNAME`
The two keys are the [default
ones](https://github.com/admc/wd#environment-variables-for-saucelabs) used by
many products related to Saucelabs. This allow us to keep the configuration
fairly straightforward and simple.
After logging in to https://saucelabs.com/, you can find your **api key** under
the **My Account**. Copy paste the value in your `.env` file.
## Anatomy of a test
First, you need to learn how [Mocha](https://mochajs.org/) works. Brew a coffee
(or tea, if coffee is not your cup of tea), sit down and read the docs.
Done? Great, let's move on and analyze how a test is written.
From a very high level, the flow of a test is the following:
1. load a page with a specific URL
2. do something on the page (click a button, submit a form, etc.)
3. maybe wait some seconds, or wait if something has changed
4. check if the new page contains some text you expect to be there
This is not set in stone, so go crazy if you want. But keep in mind that we
have a one page application, there might be some gotchas on how to wait for
stuff to happen. I suggest you to read the section [Wait for
something](https://github.com/admc/wd#waiting-for-something) to understand
better which tools you have to solve this problem.
Again, take a look to the [`wd` implementation of the JsonWire
protocol](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md)
to know all the methods you can use to control the browser.
Import the libraries we need.
```javascript
'use strict';
require('dotenv').load();
const wd = require('wd');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
```
Set up `chai` to use `chaiAsPromised`.
```javascript
chai.use(chaiAsPromised);
chai.should();
```
`browser` is the main object to interact with Saucelab "real" browsers. We will
use this object a lot. It allows us to load pages, click around, check if a
specific text is present, etc.
```javascript
describe('Login logs users in', function() {
let browser;
```
Create the driver to control the browser. `before` will be executed once at the
start of the test before any `it` functions. Use `beforeEach` instead if you'd
like to run some code before each `it` function.
```javascript
before(function() {
browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80);
// Start the browser, go to /login, and wait for the react app to render
return browser
.init({ browserName, version, platform })
.get(config.APP_URL + '/login')
.waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000);
});
```
Close the browser after finishing all tests. `after` will be executed at the end
of all `it` functions. Use `afterEach` instead if you'd like to run some code
after each `it` function.
```javascript
after(function() {
// Destroys the browser session
return browser.quit();
});
```
The actual test. We query the `browser` object to get the title of the page.
Note that `.title()` returns a `promise` **but**, since we are using
`chaiAsPromised`, we have some syntactic sugar to handle the promise in line,
without writing new functions.
```javascript
it('should contain "Log in" in the title', function() {
return browser.title().should.become('Log in');
});
});
```
All together:
```javascript
function testSuite(browserName, version, platform) {
describe(`[${browserName} ${version} ${platform}] Login logs users in`, function() {
// Set timeout to zero so Mocha won't time out.
this.timeout(0);
let browser;
before(function() {
// No need to inject `username` or `access_key`, by default the constructor
// looks up the values in `process.env.SAUCE_USERNAME` and `process.env.SAUCE_ACCESS_KEY`
browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80);
// Start the browser, go to /login, and wait for the react app to render
return browser
.init({ browserName, version, platform })
.get(config.APP_URL + '/login')
.waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000);
});
after(function() {
return browser.quit();
});
it('should contain "Log in" in the title', function() {
return browser.title().should.become('Log in');
});
});
}
```
## How to run the test suite
To run the tests, type:
```bash
$ mocha
```
By default the test suite runs on `http://www.localhost.com:3000/`, if you
want to change the URL, change the `APP_URL` env variable.
# How to have fun
Try this!
```bash
$ mocha -R nyan
```

18
test/config.js Normal file
View File

@ -0,0 +1,18 @@
'use strict';
require('dotenv').load();
// https://code.google.com/p/selenium/wiki/DesiredCapabilities
const BROWSERS = [
'chrome,47,WINDOWS',
'chrome,46,WINDOWS',
'firefox,43,MAC',
'internet explorer,10,VISTA'
];
module.exports = {
BROWSERS: BROWSERS.map(x => x.split(',')),
APP_URL: process.env.SAUCE_DEFAULT_URL || 'http://www.localhost.com:3000'
};

50
test/setup.js Normal file
View File

@ -0,0 +1,50 @@
'use strict';
const config = require('./config');
const colors = require('colors');
const sauceConnectLauncher = require('sauce-connect-launcher');
let globalSauceProcess;
if (!process.env.SAUCE_USERNAME) {
console.log(colors.red('SAUCE_USERNAME is missing. Please check the README.md file.'));
process.exit(1); //eslint-disable-line no-process-exit
}
if (!process.env.SAUCE_ACCESS_KEY) {
console.log(colors.red('SAUCE_ACCESS_KEY is missing. Please check the README.md file.'));
process.exit(1); //eslint-disable-line no-process-exit
}
if (process.env.SAUCE_AUTO_CONNECT) {
before(function(done) {
console.log(colors.yellow('Setting up tunnel from Saucelabs to your lovely computer, will take a while.'));
// Creating the tunnel takes a bit of time. For this case we can safely disable Mocha timeouts.
this.timeout(0);
sauceConnectLauncher(function (err, sauceConnectProcess) {
if (err) {
console.error(err.message);
return;
}
globalSauceProcess = sauceConnectProcess;
done();
});
});
after(function (done) {
// Creating the tunnel takes a bit of time. For this case we can safely disable it.
this.timeout(0);
if (globalSauceProcess) {
globalSauceProcess.close(done);
}
});
} else if (config.APP_URL.match(/localhost/)) {
console.log(colors.yellow(`You are running tests on ${config.APP_URL}, make sure you already have a tunnel running.`));
console.log(colors.yellow('To create the tunnel, run:'));
console.log(colors.yellow(' $ node test/tunnel.js'));
}

50
test/test-login.js Normal file
View File

@ -0,0 +1,50 @@
'use strict';
const Q = require('q');
const wd = require('wd');
const asserters = wd.asserters; // Commonly used asserters for async waits in the browser
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const config = require('./config.js');
chai.use(chaiAsPromised);
chai.should();
function testSuite(browserName, version, platform) {
describe(`[${browserName} ${version} ${platform}] Login logs users in`, function() {
// Set timeout to zero so Mocha won't time out.
this.timeout(0);
let browser;
before(function() {
// No need to inject `username` or `access_key`, by default the constructor
// looks up the values in `process.env.SAUCE_USERNAME` and `process.env.SAUCE_ACCESS_KEY`
browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80);
// Start the browser, go to /login, and wait for the react app to render
return browser
.configureHttp({ baseUrl: config.APP_URL })
.init({ browserName, version, platform })
.get('/login')
.waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000)
.catch(function (err) {
console.log('Failure -- unable to load app.');
console.log('Skipping tests for this browser...');
return Q.reject(err);
});
});
after(function() {
return browser.quit();
});
it('should contain "Log in" in the title', function() {
return browser.
waitForElementByCss('.ascribe-login-wrapper', asserters.isDisplayed, 2000)
title().should.become('Log in');
});
});
}
config.BROWSERS.map(x => testSuite(...x));

23
test/tunnel.js Normal file
View File

@ -0,0 +1,23 @@
'use strict';
const config = require('./config'); //eslint-disable-line no-unused-vars
const colors = require('colors');
const sauceConnectLauncher = require('sauce-connect-launcher');
function connect() {
console.log(colors.yellow('Setting up tunnel from Saucelabs to your lovely computer, will take a while.'));
// Creating the tunnel takes a bit of time. For this case we can safely disable Mocha timeouts.
sauceConnectLauncher(function (err) {
if (err) {
console.error(err.message);
return;
}
console.log(colors.green('Connected! Keep this process running and execute your tests.'));
});
}
if (require.main === module) {
connect();
}