├── .gitignore ├── .jshintrc ├── README.md ├── assets └── dashboard.png ├── cucumberjs-cucumberjs-webdriverio ├── README.md ├── package.json └── test │ ├── features │ ├── accounts.feature │ ├── categories.feature │ ├── reporting.feature │ └── transactions.feature │ └── step_definitions │ ├── accounts.steps.js │ ├── categories.steps.js │ ├── reporting.steps.js │ ├── support │ ├── chai-helpers.js │ ├── hooks.js │ └── world.js │ └── transactions.steps.js ├── mocha-mocha-supertest ├── README.md ├── package.json └── test │ ├── services │ ├── account.service.js │ ├── category.service.js │ ├── request.js │ └── transaction.service.js │ └── specs │ ├── accounts.spec.js │ ├── categories.spec.js │ ├── chai-helpers.js │ ├── hooks.spec.js │ ├── reporting.spec.js │ └── transactions.spec.js ├── mocha-yadda-supertest ├── .jshintrc ├── README.md ├── package.json └── test │ ├── features │ ├── accounts.feature │ ├── categories.feature │ ├── reporting.feature │ └── transactions.feature │ ├── steps │ ├── accounts.steps.js │ ├── categories.steps.js │ ├── common │ │ └── chai-helpers.js │ ├── services │ │ ├── account.service.js │ │ ├── category.service.js │ │ ├── request.js │ │ └── transaction.service.js │ └── transactions.steps.js │ └── test.js └── wdio-mocha-webdriverio ├── README.md ├── package.json ├── test ├── pages │ ├── accounts.page.js │ ├── categories.page.js │ ├── dashboard.page.js │ └── transactions.page.js └── specs │ ├── accounts.page.spec.js │ ├── categories.page.spec.js │ ├── chai-helpers.js │ ├── dashboard.page.spec.js │ ├── hooks.spec.js │ └── transactions.page.spec.js └── wdio.conf.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Visual Studio Project # 2 | ################### 3 | *.user 4 | *.gpState 5 | *.suo 6 | bin 7 | obj 8 | /packages 9 | 10 | # Ignore Node, Bower & Sass 11 | ################### 12 | node_modules 13 | bower_components 14 | build 15 | .sass-cache 16 | .tmp 17 | 18 | # Ignore Test reporters 19 | ################### 20 | **/test/coverage 21 | report 22 | 23 | 24 | # mongo db 25 | ################### 26 | #Don't commit Mongo Database files 27 | *.lock 28 | *.0 29 | *.1 30 | *.ns 31 | journal 32 | 33 | # Ignore Web Storm # 34 | .idea 35 | 36 | # Compiled source # 37 | ################### 38 | *.com 39 | *.class 40 | *.dll 41 | *.exe 42 | *.o 43 | *.so 44 | 45 | # Packages # 46 | ############ 47 | # it's better to unpack these files and commit the raw source 48 | # git has its own built in compression methods 49 | *.7z 50 | *.dmg 51 | *.gz 52 | *.iso 53 | *.jar 54 | *.rar 55 | *.tar 56 | *.xap 57 | *.zip 58 | 59 | # Logs and databases # 60 | ###################### 61 | *.log 62 | *.sql 63 | *.sqlite 64 | # *.sdf 65 | *.mdf 66 | *.ldf 67 | 68 | # OS generated files # 69 | ###################### 70 | .DS_Store* 71 | ehthumbs.db 72 | Icon? 73 | Thumbs.db 74 | 75 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Closely follows https://github.com/jshint/jshint/blob/master/examples/.jshintrc 3 | 4 | // ----- Enforcing options ----- 5 | 6 | // true: Prohibit bitwise operators (&, |, ^, etc.) 7 | "bitwise": true, 8 | 9 | // true: Identifiers must be in camelCase 10 | // Allow for snake_case identifiers 11 | "camelcase": false, 12 | 13 | // true: Require {} for every new block or scope 14 | "curly": true, 15 | 16 | // true: Require triple equals (===) for comparison 17 | "eqeqeq": true, 18 | 19 | // true: Require adherance to ECMAScript 3 specification 20 | // Set to true if you need your program to be executable in older browsers such as IE 6-9 21 | "es3": false, 22 | 23 | // true: Require filtering for..in loops with obj.hasOwnProperty() 24 | "forin": true, 25 | 26 | // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 27 | "freeze": true, 28 | 29 | // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 30 | "immed": true, 31 | 32 | // {int} Number of spaces to use for indentation 33 | "indent": 4, 34 | 35 | // true: Require variables/functions to be defined before being used 36 | // nofunc: Allow use of a function before it is defined (see https://github.com/johnpapa/angularjs-styleguide#style-y034) 37 | "latedef": "nofunc", 38 | 39 | // {int} Max cyclomatic complexity per function 40 | "maxcomplexity": 10, 41 | 42 | // {int} Max depth of nested blocks (within functions) 43 | "maxdepth": 5, 44 | 45 | // {int} Maximum error before stopping (default is 50) 46 | "maxerr": 50, 47 | 48 | // {int} Max number of characters per line 49 | "maxlen": 120, 50 | 51 | // {int} Max number of formal params allowed per function 52 | "maxparams": 10, 53 | 54 | // {int} Max number statements per function 55 | "maxstatements": 50, 56 | 57 | // true: Require capitalization of all constructor functions e.g. `new F()` 58 | "newcap": true, 59 | 60 | // true: Prohibit use of `arguments.caller` and `arguments.callee` 61 | "noarg": true, 62 | 63 | // true: prohibits the use of the comma operator 64 | // This is triggering false positives (see https://github.com/jshint/jshint/issues/2044) 65 | "nocomma": false, 66 | 67 | // true: Prohibit use of empty blocks 68 | "noempty": true, 69 | 70 | // true: Prohibit "non-breaking whitespace" characters 71 | "nonbsp": true, 72 | 73 | // true: Prohibit use of constructors for side-effects (without assignment) 74 | "nonew": true, 75 | 76 | // true: Prohibit use of `++` & `--` 77 | "plusplus": false, 78 | 79 | // Quotation mark consistency: 80 | // false : do nothing (default) 81 | // true : ensure whatever is used is consistent 82 | // "single" : require single quotes 83 | // "double" : require double quotes 84 | "quotmark": "single", 85 | 86 | // true: prohibits the use of the grouping operator for single-expression statements 87 | "singleGroups": false, 88 | 89 | // true: Requires all functions run in ES5 Strict Mode 90 | "strict": true, 91 | 92 | // true: Require all non-global variables to be declared (prevents global leaks) 93 | "undef": true, 94 | 95 | // true: Require all defined variables be used 96 | "unused": true, 97 | 98 | // ----- Relaxing options ----- 99 | 100 | // true: Tolerate Automatic Semicolon Insertion (no semicolons) 101 | "asi": false, 102 | 103 | // true: Tolerate assignments where comparisons would be expected 104 | "boss": false, 105 | 106 | // true: Allow debugger statements e.g. browser breakpoints 107 | "debug": false, 108 | 109 | // true: Tolerate use of `== null` 110 | "eqnull": false, 111 | 112 | // true: Allow ES5 syntax (ex: getters and setters) 113 | // Setting explicitly to true throws a warning: "ES5 option is now set per default" 114 | // "es5": true, 115 | 116 | // true: Allow ES.next (ES6) syntax (ex: `const`) 117 | "esnext": true, 118 | 119 | // true: Tolerate use of `eval` and `new Function()` 120 | "evil": false, 121 | 122 | // true: Tolerate `ExpressionStatement` as Programs 123 | "expr": true, 124 | 125 | // true: Tolerate defining variables inside control statements 126 | "funcscope": false, 127 | 128 | // true: Allow global "use strict" (also enables 'strict') 129 | "globalstrict": false, 130 | 131 | // true: Tolerate using the `__iterator__` property 132 | "iterator": false, 133 | 134 | // true: Tolerate omitting a semicolon for the last statement of a 1-line block 135 | "lastsemic": false, 136 | 137 | // true: Tolerate possibly unsafe line breakings 138 | "laxbreak": false, 139 | 140 | // true: Tolerate comma-first style coding 141 | "laxcomma": false, 142 | 143 | // true: Tolerate functions being defined in loops 144 | "loopfunc": false, 145 | 146 | // true: Allow Mozilla specific syntax (extends and overrides esnext features) 147 | // (ex: `for each`, multiple try/catch, function expression…) 148 | "moz": false, 149 | 150 | // true: Tolerate multi-line strings 151 | "multistr": false, 152 | 153 | // true: Tolerate invalid typeof operator values 154 | "notypeof": false, 155 | 156 | // true: Tolerate generator functions with no yield statement in them 157 | "noyield": false, 158 | 159 | // true: Tolerate using the `__proto__` property 160 | "proto": false, 161 | 162 | // true: Tolerate script-targeted URLs 163 | "scripturl": false, 164 | 165 | // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 166 | "shadow": false, 167 | 168 | // true: Tolerate using `[]` notation when it can still be expressed in dot notation 169 | "sub": false, 170 | 171 | // true: Tolerate `new function () { ... };` and `new Object;` 172 | "supernew": false, 173 | 174 | // true: Tolerate using this in a non-constructor function 175 | "validthis": false, 176 | 177 | // true: Suppresses warnings about the use of the with statement 178 | "withstmt": false, 179 | 180 | // ----- Environments ----- 181 | 182 | // Web Browser (window, document, etc) 183 | "browser": false, 184 | 185 | // Browserify (node.js code in the browser) 186 | "browserify": false, 187 | 188 | // CouchDB 189 | "couch": false, 190 | 191 | // Development/debugging (alert, confirm, etc) 192 | "devel": false, 193 | 194 | // Dojo Toolkit 195 | "dojo": false, 196 | 197 | // Jasmine 198 | "jasmine": false, 199 | 200 | // jQuery 201 | "jquery": false, 202 | 203 | // Mocha 204 | "mocha": true, 205 | 206 | // MooTools 207 | "mootools": false, 208 | 209 | // Node.js 210 | "node": true, 211 | 212 | // Widely adopted globals (escape, unescape, etc) 213 | "nonstandard": false, 214 | 215 | // PhantomJS 216 | "phantom": false, 217 | 218 | // Prototype and Scriptaculous 219 | "prototypejs": false, 220 | 221 | // QUnit 222 | "qunit": false, 223 | 224 | // Rhino 225 | "rhino": false, 226 | 227 | // ShellJS 228 | "shelljs": false, 229 | 230 | // defines globals for typed array constructors 231 | "typed": false, 232 | 233 | // Web Workers 234 | "worker": false, 235 | 236 | // Windows Scripting Host 237 | "wsh": false, 238 | 239 | // Yahoo User Interface 240 | "yui": false, 241 | 242 | // ----- Globals ----- 243 | // false: variable as read-only 244 | "globals": { 245 | "browser": false 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Acceptance Testing Best Practices 2 | 3 | *Acceptance Testing* is the process of conducting tests to determine if the requirements of a specification are met. The objective of this project is to explore best practices in acceptance testing using a variety of testing tools. 4 | 5 | We use a full-stack application called *Manage My Money* to show how real applications are tested. Manage My Money is a personal finance manager that allows you to record and analyze your income and expenses. Here's a screen shot of the application's dashboard. 6 | 7 | ![Dashboard](assets/dashboard.png) 8 | 9 | We use the [Specification-By-Example](http://www.amazon.com/gp/product/1617290084/ref=as_li_ss_tl?ie=UTF8&tag=swingwiki-20&linkCode=as2&camp=217153&creative=399701&creativeASIN=1617290084) (SBE) technique to define the requirements of the application. You can find the specification of the *Manage My Money* application [here](https://github.com/archfirst/acceptance-testing-best-practices/tree/master/cucumberjs-cucumberjs-webdriverio/test/features). We use industry leading tools such as [Cucumber](https://github.com/cucumber/cucumber/wiki/Gherkin) and [Selenium](https://seleniumhq.github.io/docs/) to drive automated testing of this specification. Each test scenario wipes out the server data before running the test. This allows each scenario to set up custom data it needs. If you do not have this level of control over your back-end you may use a different approach, e.g. use a server with a known set of test data. 10 | 11 | If there is a testing tool or framework that is not covered by this project and you are really passionate about it, please feel free to send us a pull request with your contribution. Note that you should create a separate directory for your contribution based on the guidelines below. 12 | 13 | ## Project Structure 14 | 15 | An acceptance testing environment consists of 3 primary components: 16 | 17 | 1. **Test runner**: A framework for running a collection of tests and reporting results 18 | 19 | 2. **Testing framework**: A framework for writing individual tests 20 | 21 | 3. **Front-end driver**: A api/tool used by a test to drive the front-end 22 | 23 | Many testing tools cover two or more of these aspects. For example, [cucumber-js](https://github.com/cucumber/cucumber-js) is a test runner as well as a testing framework. 24 | 25 | This project contains multiple sub-folders, each focused on a specific combination of a test runner, a testing framework and a front-end driver. For example, the folder `cucumberjs-cucumberjs-webdriverio` uses cucumber-js as the test runner as well as the testing framework, and WebdriverIO as the front-end driver. Each folder contains a README.md file that describes the specifics of the environment, e.g. the installation guide and instructions to run the tests. 26 | 27 | ## Common Setup 28 | 29 | Before running acceptance tests in any of the sub-folders, you must set up the *Manage My Money* application and common testing tools. 30 | 31 | ### Install Manage My Money 32 | 33 | The *Manage My Money* application consists of a server that runs on Node.js and an AngularJS client that runs on a web browser. 34 | 35 | - Install [Manage My Money Server](https://github.com/archfirst/manage-my-money-server). 36 | - Install [Manage My Money Client](https://github.com/archfirst/manage-my-money-client). 37 | 38 | ### Install Common Front-End Testing Tools 39 | The steps below assume that you are using a Mac as your development machine. If you are using Windows or Linux, you should be able to follow these instructions with minor modifications applicable to your platform. 40 | 41 | - Download the [Selenium Standalone Server](http://docs.seleniumhq.org/download/). As of this writing the latest version is 2.47.1 and the associated JAR file is `selenium-server-standalone-2.47.1.jar`. Move this file to a folder in your PATH. We keep it under `/usr/local/bin`. 42 | 43 | - The Selenium Standalone Server requires browser specific drivers. Download the drivers of your choice and place them under `/usr/local/bin`. 44 | - [Chrome driver](https://sites.google.com/a/chromium.org/chromedriver/home) 45 | - [PhantomJS](http://phantomjs.org/): This a headless driver that runs extremely fast compared to real browsers. There is an issue running the latest version (2.0.0) on OS X. Use `upx` to work around this problem (details [here](https://github.com/ariya/phantomjs/issues/12900)). 46 | - Other drivers: Check out the [Selenium downloads page](http://docs.seleniumhq.org/download/) 47 | 48 | You are now ready to go to any tool-specific folder and follow the instructions there to run the acceptance tests. -------------------------------------------------------------------------------- /assets/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archfirst/acceptance-testing-best-practices/b194b00a5e79a5d08d7c900e3994074089fcc0cf/assets/dashboard.png -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/README.md: -------------------------------------------------------------------------------- 1 | cucumberjs-cucumberjs-webdriverio 2 | ================================= 3 | ``` 4 | Test runner: cucumber-js 5 | Testing framework: cucumber-js 6 | Front-end driver: WebdriverIO 7 | ``` 8 | 9 | Running tests 10 | ------------- 11 | 12 | - Make sure *Manage My Money* application is running 13 | 14 | ```bash 15 | # --- In command shell 1 --- 16 | $ cd Projects/manage-my-money-server 17 | $ npm start 18 | 19 | # --- In command shell 2 --- 20 | $ cd Projects/manage-my-money-client 21 | $ gulp serve-dev 22 | ``` 23 | 24 | - Run the Selenium Standalone Server: 25 | 26 | ```bash 27 | # --- In command shell 3 --- 28 | $ java -jar /usr/local/bin/selenium-server-standalone-2.47.1.jar 29 | ``` 30 | 31 | - Run the tests: 32 | 33 | ```bash 34 | # --- In command shell 4 --- 35 | $ cd 36 | $ npm install 37 | $ npm test 38 | ``` 39 | 40 | By default, the tests run in Chrome. To switch to another browser, change the `browserName` in `test/step_definitions/support/hooks.js`. 41 | 42 | Skipping specific tests 43 | ----------------------- 44 | If you want to skip testing an entire feature or a specific scenario, simply tag it with @todo. For example: 45 | 46 | ``` 47 | @todo 48 | Scenario: Account Creation 49 | Given an account called "Cash" 50 | 51 | Then the account list should show an account called "Cash" 52 | ``` 53 | 54 | Running only specific tests 55 | --------------------------- 56 | If you are working on a feature or a scenarion and only want to run that feature or scenario, tag it with @wip. For example: 57 | 58 | ``` 59 | @wip 60 | Scenario: Account Creation 61 | Given an account called "Cash" 62 | 63 | Then the account list should show an account called "Cash" 64 | ``` 65 | 66 | Now run the test using the following command 67 | ```bash 68 | ./node_modules/cucumber/bin/cucumber.js ./test/features/ --require ./test/step_definitions/ --format pretty --tags @wip 69 | ``` 70 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cucumberjs-cucumberjs-webdriverio", 3 | "description": "cucumberjs-cucumberjs-webdriverio", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/archfirst/acceptance-testing-best-practices.git" 8 | }, 9 | "scripts": { 10 | "test": "./node_modules/cucumber/bin/cucumber.js ./test/features/ --require ./test/step_definitions/ --format pretty --tags ~@wip --tags ~@todo" 11 | }, 12 | "devDependencies": { 13 | "bluebird": "^2.9.34", 14 | "chai": "^3.0.0", 15 | "chai-as-promised": "^5.1.0", 16 | "chai-datetime": "^1.4.0", 17 | "chai-stats": "^0.3.0", 18 | "cucumber": "^0.5.1", 19 | "knex": "^0.8.6", 20 | "pg": "^4.4.0", 21 | "webdriverio": "^3.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/features/accounts.feature: -------------------------------------------------------------------------------- 1 | Feature: Accounts 2 | Story 3 | ----- 4 | In order to organize my transactions, 5 | as a user, 6 | I want the ability to create and update accounts. 7 | 8 | Acceptance Criteria 9 | ------------------- 10 | - User can create accounts 11 | - User can update accounts 12 | 13 | 14 | Scenario: Account Creation 15 | Given an account called "Cash" 16 | 17 | Then the account list should show an account called "Cash" 18 | 19 | 20 | Scenario: Account Update 21 | Given an account called "BofA Checking" 22 | 23 | When I change the account name to "Bank of America Checking" 24 | 25 | Then the account list should show an account called "Bank of America Checking" 26 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/features/categories.feature: -------------------------------------------------------------------------------- 1 | Feature: Categories 2 | Story 3 | ----- 4 | In order to analyze my income and expenses, 5 | as a user, 6 | I want the ability to create and update categories. 7 | 8 | Acceptance Criteria 9 | ------------------- 10 | - User can create categories 11 | - User can update categories 12 | 13 | 14 | Scenario: Category Creation 15 | Given a category called "Shopping" 16 | 17 | Then the category list should show a category called "Shopping" 18 | 19 | 20 | Scenario: Category Update 21 | Given a category called "Shopping" 22 | 23 | When I change the category name to "General Shopping" 24 | 25 | Then the category list should show a category called "General Shopping" 26 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/features/reporting.feature: -------------------------------------------------------------------------------- 1 | Feature: Reporting 2 | Story 3 | ----- 4 | In order to analyze my financial activities, 5 | as a user, 6 | I want the ability to summarize my transactions in different ways. 7 | 8 | Acceptance Criteria 9 | ------------------- 10 | - User can group transactions by category 11 | 12 | Background: 13 | Given the following accounts 14 | | name | 15 | | Cash | 16 | | Credit | 17 | 18 | And the following categories 19 | | name | 20 | | Auto & Transport | 21 | | Food & Dining | 22 | 23 | Scenario: Transactions By Category 24 | Given the following transactions 25 | | account | date | payee | memo | category | payment | deposit | 26 | | Cash | 01/01/2015 | Gas Station | Gas | Auto & Transport | 10.00 | | 27 | | Credit | 01/02/2015 | Grocery Store | Food | Food & Dining | 20.00 | | 28 | | Cash | 01/03/2015 | Gas Station | Gas | Auto & Transport | 30.00 | | 29 | | Credit | 01/04/2015 | Grocery Store | Food | Food & Dining | 40.00 | | 30 | | Cash | 01/05/2015 | Gas Station | Gas | Auto & Transport | 50.00 | | 31 | | Credit | 01/06/2015 | Grocery Store | Food | Food & Dining | 60.00 | | 32 | 33 | When I ask for transactions by category 34 | 35 | Then I should get the following summary of transactions by category 36 | | category | payment | 37 | | Auto & Transport | 90.00 | 38 | | Food & Dining | 120.00 | 39 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/features/transactions.feature: -------------------------------------------------------------------------------- 1 | Feature: Transactions 2 | Story 3 | ----- 4 | In order to track my financial activities, 5 | as a user, 6 | I want the ability to create, update and delete transactions in an account. 7 | 8 | Acceptance Criteria 9 | ------------------- 10 | - User can create transactions 11 | - User can update transactions 12 | - User can delete transactions 13 | 14 | Background: 15 | Given an account called "Amazon VISA" 16 | And a category called "Auto & Transport" 17 | 18 | Scenario: Transaction Creation 19 | Given a transaction with the following properties 20 | | account | date | payee | memo | category | payment | deposit | 21 | | Amazon VISA | 02/01/2015 | Chevron Gas Station | Gas | Auto & Transport | 30.00 | | 22 | 23 | Then the transaction list should show the following transaction 24 | | account | date | payee | memo | category | payment | deposit | 25 | | Amazon VISA | 02/01/2015 | Chevron Gas Station | Gas | Auto & Transport | 30.00 | | 26 | 27 | 28 | Scenario: Transaction Update 29 | Given a transaction with the following properties 30 | | account | date | payee | memo | category | payment | deposit | 31 | | Amazon VISA | 02/01/2015 | Chevron Gas Station | Gas | Auto & Transport | 30.00 | | 32 | 33 | When I change the payment amount to 50.00 34 | 35 | Then the transaction list should show the following transaction 36 | | account | date | payee | memo | category | payment | deposit | 37 | | Amazon VISA | 02/01/2015 | Chevron Gas Station | Gas | Auto & Transport | 50.00 | | 38 | 39 | 40 | Scenario: Transaction Deletion 41 | Given a transaction with the following properties 42 | | account | date | payee | memo | category | payment | deposit | 43 | | Amazon VISA | 02/01/2015 | Chevron Gas Station | Gas | Auto & Transport | 30.00 | | 44 | 45 | When I delete the transaction 46 | 47 | Then the transaction should not exist 48 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/step_definitions/accounts.steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function() { 4 | 5 | this.World = require('./support/world').World; 6 | 7 | this.Given(/^an account called "([^"]*)"$/, function(name, callback) { 8 | this.createAccount(name, callback); 9 | }); 10 | 11 | this.Given(/^the following accounts$/, function (table, callback) { 12 | this.createAccounts(table.hashes(), callback); 13 | }); 14 | 15 | this.When(/^I change the account name to "([^"]*)"$/, function (newName, callback) { 16 | this.changeAccountName(newName, callback); 17 | }); 18 | 19 | this.Then(/^the account list should show an account called "([^"]*)"$/, function (name, callback) { 20 | this.assertAccountExists(name, callback); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/step_definitions/categories.steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function() { 4 | 5 | this.World = require('./support/world').World; 6 | 7 | this.Given(/^a category called "([^"]*)"$/, function (name, callback) { 8 | this.createCategory(name, callback); 9 | }); 10 | 11 | this.Given(/^the following categories$/, function (table, callback) { 12 | this.createCategories(table.hashes(), callback); 13 | }); 14 | 15 | this.When(/^I change the category name to "([^"]*)"$/, function (newName, callback) { 16 | this.changeCategoryName(newName, callback); 17 | }); 18 | 19 | this.Then(/^the category list should show a category called "([^"]*)"$/, function (name, callback) { 20 | this.assertCategoryExists(name, callback); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/step_definitions/reporting.steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function() { 4 | 5 | this.World = require('./support/world').World; 6 | 7 | this.When(/^I ask for transactions by category$/, function(callback) { 8 | this.goToDashboard(callback); 9 | }); 10 | 11 | this.Then(/^I should get the following summary of transactions by category$/, function(table, callback) { 12 | this.assertTransactionsByCategory(table.hashes(), callback); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/step_definitions/support/chai-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | 5 | // Extend Chai with assertions about promises 6 | chai.use(require('chai-as-promised')); 7 | 8 | // Matchers for common date comparisons 9 | chai.use(require('chai-datetime')); 10 | 11 | // Statistical and numerical assertions such as .almost.equal 12 | chai.use(require('chai-stats')); 13 | 14 | // Exports 15 | module.exports = { 16 | expect: chai.expect, 17 | should: chai.should() 18 | }; 19 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/step_definitions/support/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webdriverio = require('webdriverio'); 4 | var client = null; 5 | var knex = null; 6 | 7 | var myHooks = function() { 8 | 9 | // No World instance available on `this` here 10 | this.BeforeFeatures(function(event, callback) { 11 | initKnex(); 12 | initClient(callback); 13 | }); 14 | 15 | // World instance available on `this` here 16 | this.Before(function(callback) { 17 | 18 | // Set the client on the world 19 | this.client = client; 20 | 21 | // Truncate tables 22 | knex.raw('truncate table accounts, categories, transactions cascade') 23 | .then(function() { 24 | callback(); 25 | }) 26 | .catch(function(e) { 27 | console.error(e); 28 | }); 29 | }); 30 | 31 | // No World instance available on `this` here 32 | this.AfterFeatures(function(event, callback) { 33 | 34 | // Destroy the database connection pool and close the browser 35 | if (knex && knex.client) { 36 | knex.destroy(function() { 37 | client.end().call(callback); 38 | }); 39 | } 40 | else { 41 | client.end().call(callback); 42 | } 43 | }); 44 | 45 | }; 46 | 47 | // -------------------------------------------------------- 48 | // Initialize knex (the query builder) 49 | // -------------------------------------------------------- 50 | var initKnex = function() { 51 | 52 | knex = require('knex')({ 53 | client: 'postgresql', 54 | debug: false, 55 | connection: { 56 | host: 'localhost', 57 | user: '', 58 | password: '', 59 | database: 'manage-my-money', 60 | charset: 'utf8' 61 | } 62 | }); 63 | }; 64 | 65 | // -------------------------------------------------------- 66 | // Initialize Selenium Client 67 | // -------------------------------------------------------- 68 | var initClient = function(callback) { 69 | 70 | var options = { 71 | desiredCapabilities: { 72 | browserName: 'chrome' 73 | }, 74 | logLevel:'silent', 75 | baseUrl: 'http://localhost:3000' 76 | }; 77 | 78 | client = webdriverio 79 | .remote(options) 80 | .init() 81 | .setViewportSize({width: 1024, height: 768}, true) 82 | .getViewportSize().then(function(size) { 83 | console.log(size); 84 | }) 85 | .call(callback); 86 | }; 87 | 88 | module.exports = myHooks; -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/step_definitions/support/world.js: -------------------------------------------------------------------------------- 1 | /* jshint expr: true */ 2 | 'use strict'; 3 | 4 | var Promise = require('bluebird'); 5 | var expect = require('./chai-helpers').expect; 6 | 7 | var World = function World(callback) { 8 | // ----- Cached objects ----- 9 | this.account = undefined; 10 | this.category = undefined; 11 | this.transaction = undefined; 12 | 13 | // ----- Accounts ----- 14 | this.createAccount = function(name, callback) { 15 | this.account = { name: name }; 16 | this.client 17 | .url('/settings/accounts') 18 | .setValue('.accountAddForm-name', name) 19 | .click('.accountAddForm button') 20 | .call(callback); 21 | }; 22 | 23 | this.createAccounts = function(accounts, callback) { 24 | 25 | var self = this; 26 | 27 | this.client 28 | .url('/settings/accounts') 29 | .then(function() { 30 | return Promise.each(accounts, function(account) { 31 | return self.client 32 | .setValue('.accountAddForm-name', account.name) 33 | .click('.accountAddForm button'); 34 | }); 35 | }) 36 | .call(callback); 37 | }; 38 | 39 | this.changeAccountName = function(newName, callback) { 40 | 41 | // This approach has a lot of repetition because we are not saving the list-item element 42 | // See the alternate approach below. 43 | //this.client 44 | // .url('/settings/accounts') 45 | // .element('.accountView=' + this.account.name).click('..') 46 | // .element('.accountView=' + this.account.name).element('..').setValue('.accountForm-name', newName) 47 | // .element('.accountView=' + this.account.name).element('..').submitForm('.accountForm') 48 | // .call(callback); 49 | 50 | var self = this; 51 | 52 | var liElement = null; 53 | var accountFormNameElement = null; 54 | 55 | this.client 56 | .url('/settings/accounts') 57 | .element('.accountView=' + this.account.name).element('..') 58 | .then(function(res) { 59 | // Click the list item 60 | liElement = res.value.ELEMENT; 61 | return self.client.elementIdClick(liElement); 62 | }) 63 | .then(function() { 64 | return self.client.elementIdElement(liElement, '.accountForm-name'); 65 | }) 66 | .then(function(res) { 67 | // Clear the existing account name from the form field 68 | accountFormNameElement = res.value.ELEMENT; 69 | return self.client.elementIdClear(accountFormNameElement); 70 | }) 71 | .then(function() { 72 | // Fill in the new account name 73 | return self.client.elementIdValue(accountFormNameElement, newName); 74 | }) 75 | .then(function() { 76 | return self.client.elementIdElement(liElement, '.accountForm'); 77 | }) 78 | .then(function(res) { 79 | // Submit the form 80 | return self.client.submit(res.value.ELEMENT); 81 | }) 82 | .call(callback); 83 | }; 84 | 85 | this.assertAccountExists = function(expectedName, callback) { 86 | 87 | // This approach is a bit of hand-waving because any accountView that "contains" the expected name will match 88 | //this.client 89 | // .url('/settings/accounts') 90 | // .getText('.accountView') 91 | // .should.eventually.contain(expectedName) 92 | // .notify(callback); 93 | 94 | this.client 95 | .url('/settings/accounts') 96 | .element('.accountView=' + expectedName) 97 | .call(callback); 98 | }; 99 | 100 | // ----- Categories ----- 101 | this.createCategory = function(name, callback) { 102 | this.category = { name: name }; 103 | this.client 104 | .url('/settings/categories') 105 | .setValue('.categoryAddForm-name', name) 106 | .click('.categoryAddForm button') 107 | .call(callback); 108 | }; 109 | 110 | this.createCategories = function(categories, callback) { 111 | 112 | var self = this; 113 | 114 | this.client 115 | .url('/settings/categories') 116 | .then(function() { 117 | return Promise.each(categories, function(category) { 118 | return self.client 119 | .setValue('.categoryAddForm-name', category.name) 120 | .click('.categoryAddForm button'); 121 | }); 122 | }) 123 | .call(callback); 124 | }; 125 | 126 | this.changeCategoryName = function(newName, callback) { 127 | 128 | var self = this; 129 | 130 | var liElement = null; 131 | var categoryFormNameElement = null; 132 | 133 | this.client 134 | .url('/settings/categories') 135 | .element('.categoryView=' + this.category.name).element('..') 136 | .then(function(res) { 137 | // Click the list item 138 | liElement = res.value.ELEMENT; 139 | return self.client.elementIdClick(liElement); 140 | }) 141 | .then(function() { 142 | return self.client.elementIdElement(liElement, '.categoryForm-name'); 143 | }) 144 | .then(function(res) { 145 | // Clear the existing category name from the form field 146 | categoryFormNameElement = res.value.ELEMENT; 147 | return self.client.elementIdClear(categoryFormNameElement); 148 | }) 149 | .then(function() { 150 | // Fill in the new category name 151 | return self.client.elementIdValue(categoryFormNameElement, newName); 152 | }) 153 | .then(function() { 154 | return self.client.elementIdElement(liElement, '.categoryForm'); 155 | }) 156 | .then(function(res) { 157 | // Submit the form 158 | return self.client.submit(res.value.ELEMENT); 159 | }) 160 | .call(callback); 161 | }; 162 | 163 | this.assertCategoryExists = function(expectedName, callback) { 164 | this.client 165 | .url('/settings/categories') 166 | .element('.categoryView=' + expectedName) 167 | .call(callback); 168 | }; 169 | 170 | // ----- Transaction ----- 171 | function _createTransaction(client, transaction) { 172 | 173 | var amount = (transaction.payment.length > 0) ? transaction.payment : transaction.deposit; 174 | var amountElement = (transaction.payment.length > 0) ? 'payment' : 'deposit'; 175 | var txn_date = transaction.date.replace(/\//g, ''); // remove slashes 176 | 177 | return client 178 | .url('/accounts') 179 | .element('.accountsPanel ul').click('a=' + transaction.account) 180 | .waitForExist('.transactionsPanel') 181 | .click('.transactionsPanel button') 182 | .pause(300) // wait for dialog box animation to complete 183 | .element('form[name=transactionForm]').click('#txn_date').keys(txn_date) 184 | .setValue('form[name=transactionForm] #payee', transaction.payee) 185 | .setValue('form[name=transactionForm] #memo', transaction.memo) 186 | .selectByVisibleText('form[name=transactionForm] #category', transaction.category) 187 | .setValue('form[name=transactionForm] #' + amountElement, amount) 188 | .element('form[name=transactionForm]').click('button=OK'); 189 | } 190 | 191 | this.createTransaction = function(transaction, callback) { 192 | this.transaction = transaction; 193 | 194 | _createTransaction(this.client, transaction) 195 | .call(callback); 196 | }; 197 | 198 | this.createTransactions = function(transactions, callback) { 199 | 200 | var self = this; 201 | 202 | return Promise.each(transactions, function(transaction) { 203 | return _createTransaction(self.client, transaction); 204 | }) 205 | .then(function() { 206 | callback(); 207 | }); 208 | }; 209 | 210 | this.changePaymentAmount = function(newAmount, callback) { 211 | 212 | var self = this; 213 | 214 | this.client 215 | .url('/accounts') 216 | .element('.accountsPanel ul').click('a=' + this.transaction.account) 217 | .waitForExist('.transactionsPanel') 218 | .element('.transactions-columnPayee=' + this.transaction.payee).element('..') 219 | .then(function(res) { 220 | return self.client.elementIdClick(res.value.ELEMENT); 221 | }) 222 | .pause(300) // wait for dialog box animation to complete 223 | .then(function() { 224 | return self.client.setValue('form[name=transactionForm] #payment', newAmount); 225 | }) 226 | .then(function() { 227 | return self.client.element('form[name=transactionForm]').click('button=OK'); 228 | }) 229 | .call(callback); 230 | }; 231 | 232 | this.deleteTransaction = function(callback) { 233 | 234 | var self = this; 235 | 236 | this.client 237 | .url('/accounts') 238 | .element('.accountsPanel ul').click('a=' + this.transaction.account) 239 | .waitForExist('.transactionsPanel') 240 | .element('.transactions-columnPayee=' + this.transaction.payee).element('..') 241 | .then(function(res) { 242 | return self.client.elementIdClick(res.value.ELEMENT); 243 | }) 244 | .pause(300) // wait for dialog box animation to complete 245 | .then(function() { 246 | return self.client.element('form[name=transactionForm]').click('button=Delete'); 247 | }) 248 | .call(callback); 249 | }; 250 | 251 | /** 252 | * Note: We skip the checking of memo and category fields because they are not shown in smaller widths. 253 | */ 254 | this.assertTransactionExists = function(txn, callback) { 255 | 256 | var self = this; 257 | 258 | // Table row that contains the expectedTransaction 259 | var row = null; 260 | 261 | var amount = (txn.payment.length > 0) ? txn.payment : txn.deposit; 262 | var amountElement = (txn.payment.length > 0) ? '.transactions-columnPayment' : '.transactions-columnDeposit'; 263 | 264 | this.client 265 | .url('/accounts') 266 | .element('.accountsPanel ul').click('a=' + txn.account) 267 | .waitForExist('.transactionsPanel') 268 | .element('.transactions-columnPayee=' + txn.payee).element('..') 269 | .then(function(res) { 270 | row = res.value.ELEMENT; 271 | self.client.elementIdElement(row, '.transactions-columnDate') 272 | .getText() 273 | .should.eventually.equal(txn.date); 274 | }) 275 | .then(function() { 276 | self.client.elementIdElement(row, amountElement) 277 | .getText() 278 | .should.eventually.equal(amount) 279 | .notify(callback); 280 | }); 281 | }; 282 | 283 | this.assertTransactionDoesNotExist = function(callback) { 284 | 285 | this.client 286 | .url('/accounts') 287 | .element('.accountsPanel ul').click('a=' + this.transaction.account) 288 | .waitForExist('.transactionsPanel') 289 | .element('.transactions-columnPayee=' + this.transaction.payee) 290 | .then(callback.fail, callback.bind(null, null)); 291 | }; 292 | 293 | // ----- Reporting ----- 294 | this.goToDashboard = function(callback) { 295 | this.client 296 | .url('/') 297 | .selectByVisibleText('.txnFilterForm-period', 'All time') 298 | .call(callback); 299 | }; 300 | 301 | // A very lightweight test that checks for the number of horizontal bars 302 | this.assertTransactionsByCategory = function(categories, callback) { 303 | this.client 304 | .elements('.chart .plot .bar') 305 | .then(function(res) { 306 | expect(res.value.length).to.equal(categories.length); 307 | }) 308 | .call(callback); 309 | }; 310 | 311 | callback(); 312 | }; 313 | 314 | module.exports.World = World; 315 | -------------------------------------------------------------------------------- /cucumberjs-cucumberjs-webdriverio/test/step_definitions/transactions.steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function() { 4 | 5 | this.World = require('./support/world').World; 6 | 7 | this.Given(/^a transaction with the following properties$/, function(table, callback) { 8 | this.createTransaction(table.hashes()[0], callback); 9 | }); 10 | 11 | this.Given(/^the following transactions$/, function (table, callback) { 12 | this.createTransactions(table.hashes(), callback); 13 | }); 14 | 15 | this.When(/^I change the payment amount to (\d*\.?\d*)$/, function (amount, callback) { 16 | this.changePaymentAmount(amount, callback); 17 | }); 18 | 19 | this.When(/^I delete the transaction$/, function (callback) { 20 | this.deleteTransaction(callback); 21 | }); 22 | 23 | this.Then(/^the transaction list should show the following transaction$/, function (table, callback) { 24 | this.assertTransactionExists(table.hashes()[0], callback); 25 | }); 26 | 27 | this.Then(/^the transaction should not exist$/, function (callback) { 28 | this.assertTransactionDoesNotExist(callback); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/README.md: -------------------------------------------------------------------------------- 1 | mocha-mocha-supertest 2 | ===================== 3 | 4 | This is a special project to demonstrate the acceptance testing of the *Manage My Money* server API. As such we don't 5 | need a front-end driver. Instead we will use [SuperTest](https://github.com/visionmedia/supertest) to drive the REST API. 6 | Also note that the acceptance tests have been worded in terms of the API. You will find the Cucumber version 7 | [here](https://github.com/archfirst/manage-my-money-server/tree/master/test/features). 8 | 9 | ``` 10 | Test runner: mocha 11 | Testing framework: mocha 12 | HTTP driver: supertest/supertest-as-promised 13 | ``` 14 | 15 | Running tests 16 | ------------- 17 | 18 | - Make sure that mocha is installed globally 19 | 20 | ```bash 21 | $ npm install -g mocha 22 | ``` 23 | 24 | - Make sure *Manage My Money* application is running 25 | 26 | ```bash 27 | # --- In command shell 1 --- 28 | $ cd Projects/manage-my-money-server 29 | $ npm start 30 | ``` 31 | 32 | - Run the tests: 33 | 34 | ```bash 35 | # --- In command shell 2 --- 36 | $ cd 37 | $ npm install 38 | $ npm test 39 | ``` 40 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-mocha-supertest", 3 | "description": "mocha-mocha-supertest", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/archfirst/acceptance-testing-best-practices.git" 8 | }, 9 | "scripts": { 10 | "test": "mocha test/specs/" 11 | }, 12 | "devDependencies": { 13 | "bluebird": "^2.10.1", 14 | "chai": "^3.0.0", 15 | "chai-as-promised": "^5.1.0", 16 | "chai-datetime": "^1.4.0", 17 | "chai-stats": "^0.3.0", 18 | "knex": "^0.8.6", 19 | "lodash": "^3.10.1", 20 | "pg": "^4.4.0", 21 | "supertest-as-promised": "^2.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/services/account.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | createAccount: createAccount, 5 | createAccounts: createAccounts, 6 | updateAccount: updateAccount, 7 | getAccount: getAccount, 8 | deleteAccount: deleteAccount 9 | }; 10 | 11 | var _ = require('lodash'); 12 | var Promise = require('bluebird'); 13 | var request = require('./request'); 14 | 15 | /** 16 | * Creates a new account. 17 | * 18 | * @param {string} name 19 | * @returns {Promise} A promise that returns the created account 20 | */ 21 | function createAccount(name) { 22 | 23 | return request.post('/accounts') 24 | .send({ 25 | name: name 26 | }) 27 | .expect(200) 28 | .then(function(res) { 29 | return res.body; 30 | }); 31 | } 32 | 33 | /** 34 | * Creates accounts. 35 | * 36 | * @param {string[]} accountNames 37 | * @returns {Promise} A promise that returns the created accounts 38 | */ 39 | function createAccounts(names) { 40 | 41 | var tasks = []; 42 | _.each(names, function(name) { 43 | tasks.push(request.post('/accounts').send({name: name}).expect(200)); 44 | }); 45 | 46 | return Promise.all(tasks) 47 | .then(function(responses) { 48 | return _.map(responses, function(response) { 49 | return response.body; 50 | }) 51 | }); 52 | } 53 | 54 | /** 55 | * Updates an existing account. 56 | * 57 | * @param {string} name 58 | * @returns {Promise} A promise that returns the created account 59 | */ 60 | function updateAccount(account) { 61 | 62 | return request.put('/accounts/' + account.id) 63 | .send(account) 64 | .expect(200) 65 | .then(function(res) { 66 | return res.body; 67 | }); 68 | } 69 | 70 | /** 71 | * Gets an account. 72 | * 73 | * @param {number} id 74 | * @returns {Promise} A promise that returns the desired account 75 | * @throws {String} "Not found" if account is not found 76 | */ 77 | function getAccount(id) { 78 | 79 | return request.get('/accounts/' + id) 80 | .expect(200) 81 | .then(function(res) { 82 | return res.body; 83 | }); 84 | } 85 | 86 | /** 87 | * Deletes an account. 88 | * 89 | * @param {number} id 90 | * @returns {Promise} A promise that returns true when the account is deleted 91 | */ 92 | function deleteAccount(id) { 93 | 94 | return request.delete('/accounts/' + id) 95 | .expect(204) 96 | .then(function() { 97 | return true; 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/services/category.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | createCategory: createCategory, 5 | createCategories: createCategories, 6 | updateCategory: updateCategory, 7 | getCategory: getCategory, 8 | deleteCategory: deleteCategory 9 | }; 10 | 11 | var _ = require('lodash'); 12 | var Promise = require('bluebird'); 13 | var request = require('./request'); 14 | 15 | /** 16 | * Creates a new category. 17 | * 18 | * @param {string} name 19 | * @returns {Promise} A promise that returns the created category 20 | */ 21 | function createCategory(name) { 22 | 23 | return request.post('/categories') 24 | .send({ 25 | name: name 26 | }) 27 | .expect(200) 28 | .then(function(res) { 29 | return res.body; 30 | }); 31 | } 32 | 33 | /** 34 | * Creates categories. 35 | * 36 | * @param {string[]} categoryNames 37 | * @returns {Promise} A promise that returns the created categories 38 | */ 39 | function createCategories(names) { 40 | 41 | var tasks = []; 42 | _.each(names, function(name) { 43 | tasks.push(request.post('/categories').send({name: name}).expect(200)); 44 | }); 45 | 46 | return Promise.all(tasks) 47 | .then(function(responses) { 48 | return _.map(responses, function(response) { 49 | return response.body; 50 | }) 51 | }); 52 | } 53 | 54 | /** 55 | * Updates an existing category. 56 | * 57 | * @param {string} name 58 | * @returns {Promise} A promise that returns the created category 59 | */ 60 | function updateCategory(category) { 61 | 62 | return request.put('/categories/' + category.id) 63 | .send(category) 64 | .expect(200) 65 | .then(function(res) { 66 | return res.body; 67 | }); 68 | } 69 | 70 | /** 71 | * Gets a category. 72 | * 73 | * @param {number} id 74 | * @returns {Promise} A promise that returns the desired category 75 | * @throws {String} "Not found" if category is not found 76 | */ 77 | function getCategory(id) { 78 | 79 | return request.get('/categories/' + id) 80 | .expect(200) 81 | .then(function(res) { 82 | return res.body; 83 | }); 84 | } 85 | 86 | /** 87 | * Deletes a category. 88 | * 89 | * @param {number} id 90 | * @returns {Promise} A promise that returns true when the category is deleted 91 | */ 92 | function deleteCategory(id) { 93 | 94 | return request.delete('/categories/' + id) 95 | .expect(204) 96 | .then(function() { 97 | return true; 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/services/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('supertest-as-promised'); 4 | 5 | request = request('http://localhost:8080'); 6 | 7 | module.exports = request; 8 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/services/transaction.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | createTransaction: createTransaction, 5 | createTransactions: createTransactions, 6 | updateTransaction: updateTransaction, 7 | getTransaction: getTransaction, 8 | getTransactionsByCategory: getTransactionsByCategory, 9 | deleteTransaction: deleteTransaction 10 | }; 11 | 12 | var _ = require('lodash'); 13 | var Promise = require('bluebird'); 14 | var request = require('./request'); 15 | 16 | /** 17 | * Creates a new transaction. 18 | * 19 | * @param {Transaction} transaction 20 | * @returns {Promise} A promise that returns the created transaction 21 | */ 22 | function createTransaction(transaction) { 23 | 24 | return request.post('/transactions') 25 | .send(transaction) 26 | .expect(200) 27 | .then(function(res) { 28 | return res.body; 29 | }); 30 | } 31 | 32 | /** 33 | * Creates transactions. 34 | * 35 | * @param {Transaction[]} transactions 36 | * @returns {Promise} A promise that returns the created transactions 37 | */ 38 | function createTransactions(transactions) { 39 | 40 | var tasks = []; 41 | _.each(transactions, function(transaction) { 42 | tasks.push(request.post('/transactions').send(transaction).expect(200)); 43 | }); 44 | 45 | return Promise.all(tasks) 46 | .then(function(responses) { 47 | return _.map(responses, function(response) { 48 | return response.body; 49 | }) 50 | }); 51 | } 52 | 53 | /** 54 | * Updates an existing transaction. 55 | * 56 | * @param {string} name 57 | * @returns {Promise} A promise that returns the created transaction 58 | */ 59 | function updateTransaction(transaction) { 60 | 61 | return request.put('/transactions/' + transaction.id) 62 | .send(transaction) 63 | .expect(200) 64 | .then(function(res) { 65 | return res.body; 66 | }); 67 | } 68 | 69 | /** 70 | * Gets a transaction. 71 | * 72 | * @param {number} id 73 | * @returns {Promise} A promise that returns the desired transaction 74 | * @throws {String} "Not found" if transaction is not found 75 | */ 76 | function getTransaction(id) { 77 | 78 | return request.get('/transactions/' + id) 79 | .expect(200) 80 | .then(function(res) { 81 | return res.body; 82 | }); 83 | } 84 | 85 | /** 86 | * Gets transactions by category. 87 | * 88 | * @param startDate 89 | * @param endDate 90 | * @returns {Promise} A promise that returns categories with total amounts. 91 | */ 92 | function getTransactionsByCategory(startDate, endDate) { 93 | 94 | var dateRange = '&startDate=' + startDate.toISOString() + '&endDate=' + endDate.toISOString(); 95 | 96 | return request.get('/transactions?groupByCategory' + dateRange) 97 | .expect(200) 98 | .then(function(response) { 99 | return response.body; 100 | }); 101 | } 102 | 103 | /** 104 | * Deletes a transaction. 105 | * 106 | * @param {number} id 107 | * @returns {Promise} A promise that returns true when the transaction is deleted 108 | */ 109 | function deleteTransaction(id) { 110 | 111 | return request.delete('/transactions/' + id) 112 | .expect(204) 113 | .then(function() { 114 | return true; 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/specs/accounts.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('./chai-helpers').expect; 4 | var accountService = require('../services/account.service'); 5 | 6 | describe('Accounts API', function() { 7 | 8 | // ----- Account Creation ----- 9 | // When I create an account called "Cash" 10 | // And I ask for the account 11 | // Then I should get the account called "Cash" 12 | it('should allow the creation of an account', function() { 13 | 14 | // When I create an account called "Cash" 15 | return accountService.createAccount('Cash') 16 | 17 | // And I ask for the account 18 | .then(function(account) { 19 | return accountService.getAccount(account.id); 20 | }) 21 | 22 | // Then I should get the account called "Cash" 23 | .then(function(account) { 24 | expect(account.name).to.equal('Cash'); 25 | }); 26 | }); 27 | 28 | // ----- Account Update ----- 29 | // Given an account called "BofA Checking" 30 | // When I change the account name to "Bank of America Checking" 31 | // And I ask for the account 32 | // Then I should get the account called "Bank of America Checking" 33 | it('should allow updating of an account', function() { 34 | 35 | // Given an account called "BofA Checking" 36 | return accountService.createAccount('BofA Checking') 37 | 38 | // When I change the account name to "Bank of America Checking" 39 | .then(function(account) { 40 | account.name = 'Bank of America Checking'; 41 | return accountService.updateAccount(account); 42 | }) 43 | 44 | // And I ask for the account 45 | .then(function(account) { 46 | return accountService.getAccount(account.id); 47 | }) 48 | 49 | // Then I should get the account called "Bank of America Checking" 50 | .then(function(account) { 51 | expect(account.name).to.equal('Bank of America Checking'); 52 | }); 53 | }); 54 | 55 | // ----- Account Deletion ----- 56 | // Given an account called "BofA Checking" 57 | // When I delete the account 58 | // Then the account should not exist 59 | it('should allow the deletion of an account', function() { 60 | 61 | var account = null; 62 | 63 | // Given an account called "BofA Checking" 64 | return accountService.createAccount('BofA Checking') 65 | 66 | // When I delete the account 67 | .then(function(createdAccount) { 68 | account = createdAccount; 69 | return accountService.deleteAccount(account.id); 70 | }) 71 | 72 | // Then the account should not exist 73 | .then(function() { 74 | return expect(accountService.getAccount(account.id)) 75 | .to.eventually.be.rejected; 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/specs/categories.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('./chai-helpers').expect; 4 | var categoryService = require('../services/category.service'); 5 | 6 | describe('Categories API', function() { 7 | 8 | // ----- Category Creation ----- 9 | // When I create a category called "Shopping" 10 | // And I ask for the category 11 | // Then I should get the category called "Shopping" 12 | it('should allow the creation of a category', function() { 13 | 14 | // When I create a category called "Shopping" 15 | return categoryService.createCategory('Shopping') 16 | 17 | // And I ask for the category 18 | .then(function(category) { 19 | return categoryService.getCategory(category.id); 20 | }) 21 | 22 | // Then I should get the category called "Shopping" 23 | .then(function(category) { 24 | expect(category.name).to.equal('Shopping'); 25 | }); 26 | }); 27 | 28 | // ----- Category Update ----- 29 | // Given a category called "Shopping" 30 | // When I change the category name to "General Shopping" 31 | // And I ask for the category 32 | // Then I should get the category called "General Shopping" 33 | it('should allow updating of a category', function() { 34 | 35 | // Given a category called "Shopping" 36 | return categoryService.createCategory('Shopping') 37 | 38 | // When I change the category name to "General Shopping" 39 | .then(function(category) { 40 | category.name = 'General Shopping'; 41 | return categoryService.updateCategory(category); 42 | }) 43 | 44 | // And I ask for the category 45 | .then(function(category) { 46 | return categoryService.getCategory(category.id); 47 | }) 48 | 49 | // Then I should get the category called "General Shopping" 50 | .then(function(category) { 51 | expect(category.name).to.equal('General Shopping'); 52 | }); 53 | }); 54 | 55 | // ----- Category Deletion ----- 56 | // Given a category called "Shopping" 57 | // When I delete the category 58 | // Then the category should not exist 59 | it('should allow the deletion of a category', function() { 60 | 61 | var category = null; 62 | 63 | // Given a category called "Shopping" 64 | return categoryService.createCategory('Shopping') 65 | 66 | // When I delete the category 67 | .then(function(createdCategory) { 68 | category = createdCategory; 69 | return categoryService.deleteCategory(category.id); 70 | }) 71 | 72 | // Then the category should not exist 73 | .then(function() { 74 | return expect(categoryService.getCategory(category.id)) 75 | .to.eventually.be.rejected; 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/specs/chai-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | 5 | // Extend Chai with assertions about promises 6 | chai.use(require('chai-as-promised')); 7 | 8 | // Matchers for common date comparisons 9 | chai.use(require('chai-datetime')); 10 | 11 | // Statistical and numerical assertions such as .almost.equal 12 | chai.use(require('chai-stats')); 13 | 14 | // Exports 15 | module.exports = { 16 | expect: chai.expect, 17 | should: chai.should() 18 | }; 19 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/specs/hooks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var knex = null; 4 | 5 | // Runs before all test cases. 6 | // Initializes knex. 7 | before(function() { 8 | knex = require('knex')({ 9 | client: 'postgresql', 10 | debug: false, 11 | connection: { 12 | host: 'localhost', 13 | user: '', 14 | password: '', 15 | database: 'manage-my-money', 16 | charset: 'utf8' 17 | } 18 | }); 19 | }); 20 | 21 | // Runs before each test case. 22 | // Truncates tables. 23 | beforeEach(function() { 24 | return knex.raw('truncate table accounts, categories, transactions cascade'); 25 | }); 26 | 27 | // Runs after each test case. 28 | // Does nothing. 29 | afterEach(function() { 30 | }); 31 | 32 | // Runs after all test cases. 33 | // Destroys the database connection. 34 | after(function() { 35 | if (knex && knex.client) { 36 | return knex.destroy(); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/specs/reporting.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var expect = require('./chai-helpers').expect; 5 | var accountService = require('../services/account.service'); 6 | var categoryService = require('../services/category.service'); 7 | var transactionService = require('../services/transaction.service'); 8 | 9 | describe('Reporting API', function() { 10 | 11 | var accounts = null; 12 | var categories = null; 13 | 14 | /* ----- Background ----- */ 15 | beforeEach(function() { 16 | // Given two accounts: "Cash", "Credit" 17 | return accountService.createAccounts(['Cash', 'Credit']) 18 | 19 | // And two categories "Auto & Transport", "Food & Dining" 20 | .then(function(createdAccounts) { 21 | accounts = createdAccounts; 22 | return categoryService.createCategories(['Auto & Transport', 'Food & Dining']); 23 | }) 24 | 25 | .then(function(createdCategories) { 26 | categories = createdCategories; 27 | }); 28 | }); 29 | 30 | // ----- Transactions By Category ----- 31 | it('should show a summary of transactions by category', function() { 32 | 33 | // When I create the following transactions 34 | var transactions = [ 35 | { txn_date: new Date('2015-01-01'), payee: 'Gas Station', memo: 'Gas', amount: -10.00, account_id: accounts[0].id, category_id: categories[0].id }, 36 | { txn_date: new Date('2015-01-02'), payee: 'Grocery Store', memo: 'Food', amount: -20.00, account_id: accounts[1].id, category_id: categories[1].id }, 37 | { txn_date: new Date('2015-01-03'), payee: 'Gas Station', memo: 'Gas', amount: -30.00, account_id: accounts[0].id, category_id: categories[0].id }, 38 | { txn_date: new Date('2015-01-04'), payee: 'Grocery Store', memo: 'Food', amount: -40.00, account_id: accounts[1].id, category_id: categories[1].id }, 39 | { txn_date: new Date('2015-01-05'), payee: 'Gas Station', memo: 'Gas', amount: -50.00, account_id: accounts[0].id, category_id: categories[0].id }, 40 | { txn_date: new Date('2015-01-06'), payee: 'Grocery Store', memo: 'Food', amount: -60.00, account_id: accounts[1].id, category_id: categories[1].id } 41 | ]; 42 | return transactionService.createTransactions(transactions) 43 | 44 | // When I ask for transactions by category with start date of "2015-01-02" and end date of "2015-01-05" 45 | .then(function() { 46 | return transactionService.getTransactionsByCategory(new Date('2015-01-02'), new Date('2015-01-05')); 47 | }) 48 | 49 | // Then I should get the following summary of transactions by category 50 | .then(function(returnedItems) { 51 | var expectedItems = [ 52 | { cat_id: categories[0].id, cat_name: 'Auto & Transport', amount: -80.00 }, 53 | { cat_id: categories[1].id, cat_name: 'Food & Dining', amount: -60.00 } 54 | ]; 55 | 56 | // Sort returnedItems by cat_name (expectedItems are sorted already) 57 | expect(_.sortBy(returnedItems, 'cat_name')).to.deep.equal(expectedItems); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /mocha-mocha-supertest/test/specs/transactions.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('./chai-helpers').expect; 4 | var accountService = require('../services/account.service'); 5 | var categoryService = require('../services/category.service'); 6 | var transactionService = require('../services/transaction.service'); 7 | 8 | describe('Transactions API', function() { 9 | 10 | var account = null; 11 | var category = null; 12 | 13 | function toResource(txn) { 14 | return { 15 | id: txn.id, 16 | txn_date: new Date(txn.txn_date), 17 | payee: txn.payee, 18 | memo: txn.memo, 19 | amount: txn.amount, 20 | account_id: txn.account.id, 21 | category_id: txn.category.id 22 | }; 23 | } 24 | 25 | /* ----- Background ----- */ 26 | // Given an account called "Amazon VISA" 27 | // And a category called "Auto & Transport" 28 | beforeEach(function() { 29 | // Given an account called "Amazon VISA" 30 | return accountService.createAccount('Amazon VISA') 31 | 32 | // And a category called "Auto & Transport" 33 | .then(function(createdAccount) { 34 | account = createdAccount; 35 | return categoryService.createCategory('Auto & Transport'); 36 | }) 37 | 38 | .then(function(createdCategory) { 39 | category = createdCategory; 40 | }); 41 | }); 42 | 43 | // ----- Transaction Creation ----- 44 | it('should allow the creation of a transaction', function() { 45 | 46 | // When I create a transaction with the following properties 47 | var transaction = { 48 | txn_date: new Date('2015-02-01T00:00Z'), 49 | payee: 'Chevron Gas Station', 50 | memo: 'Gas', 51 | amount: -30.00, 52 | account_id: account.id, 53 | category_id: category.id 54 | }; 55 | return transactionService.createTransaction(transaction) 56 | 57 | // And I ask for the transaction 58 | .then(function(createdTransaction) { 59 | transaction.id = createdTransaction.id; 60 | return transactionService.getTransaction(transaction.id); 61 | }) 62 | 63 | // Then I should get the same transaction back 64 | .then(function(receivedTransaction) { 65 | expect(toResource(receivedTransaction)).to.deep.equal(transaction); 66 | }); 67 | }); 68 | 69 | // ----- Transaction Update ----- 70 | it('should allow the update a transaction', function() { 71 | 72 | // When I create a transaction with the following properties 73 | var transaction = { 74 | txn_date: new Date('2015-02-01T00:00Z'), 75 | payee: 'Chevron Gas Station', 76 | memo: 'Gas', 77 | amount: -30.00, 78 | account_id: account.id, 79 | category_id: category.id 80 | }; 81 | return transactionService.createTransaction(transaction) 82 | 83 | // When I change the transaction amount to -50.00 84 | .then(function(createdTransaction) { 85 | transaction.id = createdTransaction.id; 86 | transaction.amount = -50.00; 87 | return transactionService.updateTransaction(transaction); 88 | }) 89 | 90 | // And I ask for the transaction 91 | .then(function() { 92 | return transactionService.getTransaction(transaction.id); 93 | }) 94 | 95 | // Then I should get the updated transaction back 96 | .then(function(receivedTransaction) { 97 | expect(toResource(receivedTransaction)).to.deep.equal(transaction); 98 | }); 99 | }); 100 | 101 | // ----- Transaction Deletion ----- 102 | it('should allow the deletion of a transaction', function() { 103 | 104 | // When I create a transaction with the following properties 105 | var transaction = { 106 | txn_date: new Date('2015-02-01T00:00Z'), 107 | payee: 'Chevron Gas Station', 108 | memo: 'Gas', 109 | amount: -30.00, 110 | account_id: account.id, 111 | category_id: category.id 112 | }; 113 | return transactionService.createTransaction(transaction) 114 | 115 | // When I delete the transaction 116 | .then(function(createdTransaction) { 117 | transaction.id = createdTransaction.id; 118 | return transactionService.deleteTransaction(transaction.id); 119 | }) 120 | 121 | // Then the transaction should not exist 122 | .then(function() { 123 | return expect(transactionService.getTransaction(transaction.id)) 124 | .to.eventually.be.rejected; 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | 4 | // ----- Globals ----- 5 | // false: variable as read-only 6 | "globals": { 7 | "featureFile": false, 8 | "scenarios": false, 9 | "steps": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/README.md: -------------------------------------------------------------------------------- 1 | mocha-yadda-supertest 2 | ===================== 3 | 4 | This is a special project to demonstrate the acceptance testing of the *Manage My Money* server API. As such we don't 5 | need a front-end driver. Instead we will use [SuperTest](https://github.com/visionmedia/supertest) to drive the REST API. 6 | Also note that the acceptance tests have been worded in terms of the API. You will find the Cucumber version 7 | [here](https://github.com/archfirst/manage-my-money-server/tree/master/test/features). 8 | 9 | ``` 10 | Test runner: mocha 11 | Testing framework: yadda 12 | HTTP driver: supertest/supertest-as-promised 13 | ``` 14 | 15 | Running tests 16 | ------------- 17 | 18 | - Make sure that mocha is installed globally 19 | 20 | ```bash 21 | $ npm install -g mocha 22 | ``` 23 | 24 | - Make sure *Manage My Money* application is running 25 | 26 | ```bash 27 | # --- In command shell 1 --- 28 | $ cd Projects/manage-my-money-server 29 | $ npm start 30 | ``` 31 | 32 | - Run the tests: 33 | 34 | ```bash 35 | # --- In command shell 2 --- 36 | $ cd 37 | $ npm install 38 | $ npm test 39 | ``` 40 | 41 | Yadda Configuration 42 | ------------------- 43 | Yadda can be integrated with Mocha in two ways, using the `StepLevelPlugin` or the `ScenarioLevelPlugin`. 44 | Each plugin translates features, scenarios and steps to different constructs in Mocha. 45 | 46 | - `StepLevelPlugin`: Each step is a separate Mocha test. This is generally better for output 47 | because you can see all the steps executed. 48 | - feature > describe 49 | - scenario > describe 50 | - step > it 51 | 52 | - `ScenarioLevelPlugin`: Each scenario is a separate Mocha test. You only see the scenario title in the mocha output. 53 | - feature > describe 54 | - scenario > it 55 | - step > no translation (executes as part of the scenario) 56 | 57 | We are using the `StepLevelPlugin` to integrate with Mocha. See [here](test/test.js). 58 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-yadda-supertest", 3 | "description": "mocha-yadda-supertest", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/archfirst/acceptance-testing-best-practices.git" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "devDependencies": { 13 | "bluebird": "^2.10.1", 14 | "chai": "^3.0.0", 15 | "chai-as-promised": "^5.1.0", 16 | "knex": "^0.8.6", 17 | "lodash": "^3.10.1", 18 | "mocha": "^2.3.3", 19 | "pg": "^4.4.0", 20 | "supertest-as-promised": "^2.0.2", 21 | "yadda": "^0.16.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/features/accounts.feature: -------------------------------------------------------------------------------- 1 | @libraries=accounts 2 | Feature: Accounts 3 | In order to organize my transactions, 4 | as a user, 5 | I want the ability to create, update and delete accounts. 6 | 7 | Scenario: Account Creation 8 | When I create an account called "Cash" 9 | And I ask for the account 10 | Then I should get the account called "Cash" 11 | 12 | Scenario: Account Update 13 | Given an account called "BofA Checking" 14 | When I change the account name to "Bank of America Checking" 15 | And I ask for the account 16 | Then I should get the account called "Bank of America Checking" 17 | 18 | Scenario: Account Deletion 19 | Given an account called "BofA Checking" 20 | When I delete the account 21 | Then the account should not exist 22 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/features/categories.feature: -------------------------------------------------------------------------------- 1 | @libraries=categories 2 | Feature: Categories 3 | In order to analyze my income and expenses, 4 | as a user, 5 | I want the ability to create, update and delete categories. 6 | 7 | Scenario: Category Creation 8 | When I create a category called "Shopping" 9 | And I ask for the category 10 | Then I should get the category called "Shopping" 11 | 12 | Scenario: Category Update 13 | Given a category called "Shopping" 14 | When I change the category name to "General Shopping" 15 | And I ask for the category 16 | Then I should get the category called "General Shopping" 17 | 18 | Scenario: Category Deletion 19 | Given a category called "Shopping" 20 | When I delete the category 21 | Then the category should not exist 22 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/features/reporting.feature: -------------------------------------------------------------------------------- 1 | @libraries=accounts, categories, transactions 2 | Feature: Reporting 3 | In order to analyze my financial activities, 4 | as a user, 5 | I want the ability to summarize my transactions in different ways. 6 | 7 | Background: 8 | Given the following accounts 9 | ------ 10 | Cash 11 | Credit 12 | ------ 13 | 14 | And the following categories 15 | ---------------- 16 | Auto & Transport 17 | Food & Dining 18 | ---------------- 19 | 20 | Scenario: Transactions By Category 21 | Given the following transactions 22 | ---------------------------------------------------------------------------- 23 | | date | payee | memo | amount | account | category | 24 | | 2015-01-01 | Gas Station | Gas | -10.00 | Cash | Auto & Transport | 25 | | 2015-01-02 | Grocery Store | Food | -20.00 | Credit | Food & Dining | 26 | | 2015-01-03 | Gas Station | Gas | -30.00 | Cash | Auto & Transport | 27 | | 2015-01-04 | Grocery Store | Food | -40.00 | Credit | Food & Dining | 28 | | 2015-01-05 | Gas Station | Gas | -50.00 | Cash | Auto & Transport | 29 | | 2015-01-06 | Grocery Store | Food | -60.00 | Credit | Food & Dining | 30 | ---------------------------------------------------------------------------- 31 | 32 | When I ask for transactions by category with start date of 2015-01-02 and end date of 2015-01-05 33 | 34 | Then I should get the following summary of transactions by category 35 | ------------------------------ 36 | | category | amount | 37 | | Auto & Transport | -80.00 | 38 | | Food & Dining | -60.00 | 39 | ------------------------------ 40 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/features/transactions.feature: -------------------------------------------------------------------------------- 1 | @libraries=accounts, categories, transactions 2 | Feature: Manage transactions 3 | In order to track my financial activities, 4 | as a user, 5 | I want the ability to add, change and delete transactions 6 | 7 | Background: 8 | Given the following accounts 9 | ------------- 10 | BofA Checking 11 | Amazon VISA 12 | ------------- 13 | 14 | And the following categories 15 | ---------------- 16 | Auto & Transport 17 | Salary 18 | Transfer 19 | Utilities 20 | ---------------- 21 | 22 | Scenario: Deposits can be made to bank accounts 23 | When I make the following deposit to BofA Checking 24 | ----------------------------------------- 25 | | payee | memo | amount | category | 26 | | Hooli | Paycheck | 1000.00 | Salary | 27 | ----------------------------------------- 28 | 29 | And I ask for the transaction 30 | 31 | Then I should get the following deposit 32 | ----------------------------------------- 33 | | payee | memo | amount | category | 34 | | Hooli | Paycheck | 1000.00 | Salary | 35 | ----------------------------------------- 36 | 37 | Scenario: Payments can be made from bank accounts 38 | When I make the following payment from BofA Checking 39 | ---------------------------------------------------------- 40 | | payee | memo | amount | category | 41 | | National Electric | Electric bill | 100.00 | Utilities | 42 | ---------------------------------------------------------- 43 | 44 | And I ask for the transaction 45 | 46 | Then I should get the following payment 47 | ---------------------------------------------------------- 48 | | payee | memo | amount | category | 49 | | National Electric | Electric bill | 100.00 | Utilities | 50 | ---------------------------------------------------------- 51 | 52 | Scenario: Purchases can be charged to credit cards 53 | When I make the following purchase using Amazon VISA 54 | ---------------------------------------------------------- 55 | | payee | memo | amount | category | 56 | | Chevron Gas Station | Gas | 30.00 | Auto & Transport | 57 | ---------------------------------------------------------- 58 | 59 | And I ask for the transaction 60 | 61 | Then I should get the following payment 62 | ---------------------------------------------------------- 63 | | payee | memo | amount | category | 64 | | Chevron Gas Station | Gas | 30.00 | Auto & Transport | 65 | ---------------------------------------------------------- 66 | 67 | Scenario: Credit cards can be paid off by direct transfer from other accounts 68 | When I make the following payment from BofA Checking 69 | ----------------------------------------------------------- 70 | | payee | memo | amount | category | 71 | | Amazon VISA | Pay off monthly bill | 1011.66 | Transfer | 72 | ----------------------------------------------------------- 73 | 74 | And I ask for the transaction 75 | 76 | Then I should get the following payment 77 | ----------------------------------------------------------- 78 | | payee | memo | amount | category | 79 | | Amazon VISA | Pay off monthly bill | 1011.66 | Transfer | 80 | ----------------------------------------------------------- 81 | 82 | Scenario: Transactions can be corrected 83 | When I make the following purchase using Amazon VISA 84 | ---------------------------------------------------------- 85 | | payee | memo | amount | category | 86 | | Chevron Gas Station | Gas | 30.00 | Auto & Transport | 87 | ---------------------------------------------------------- 88 | 89 | And I change the purchase amount to 40.00 90 | 91 | And I ask for the transaction 92 | 93 | Then I should get the following payment 94 | ---------------------------------------------------------- 95 | | payee | memo | amount | category | 96 | | Chevron Gas Station | Gas | 40.00 | Auto & Transport | 97 | ---------------------------------------------------------- 98 | 99 | Scenario: Transactions can be deleted 100 | Given a transaction with the following properties 101 | ------------------------------------------------------------------------------------- 102 | | date | payee | memo | amount | account | category | 103 | | 2015-02-01 | Chevron Gas Station | Gas | -30.00 | Amazon VISA | Auto & Transport | 104 | ------------------------------------------------------------------------------------- 105 | 106 | When I delete the transaction 107 | 108 | Then the transaction should not exist 109 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/accounts.steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Yadda = require('yadda'); 4 | var expect = require('./common/chai-helpers').expect; 5 | var accountService = require('./services/account.service'); 6 | 7 | var English = Yadda.localisation.English; 8 | var Dictionary = Yadda.Dictionary; 9 | 10 | var dictionary = new Dictionary() 11 | .define('list', /([^\u0000]*)/, Yadda.converters.list); 12 | 13 | module.exports = English.library(dictionary) 14 | 15 | .when('I create an account called "$accountName"', function(accountName, next) { 16 | var self = this; 17 | accountService.createAccount(accountName) 18 | .then(function(createdAccount) { 19 | self.ctx.account = createdAccount; 20 | next(); 21 | }); 22 | }) 23 | 24 | .when('I ask for the account', function(next) { 25 | var self = this; 26 | accountService.getAccount(this.ctx.account.id) 27 | .then(function(receivedAccount) { 28 | self.ctx.account = receivedAccount; 29 | next(); 30 | }); 31 | }) 32 | 33 | .then('I should get the account called "$accountName"', function(accountName, next) { 34 | expect(this.ctx.account.name).to.equal(accountName); 35 | next(); 36 | }) 37 | 38 | .given('an account called "$accountName"', function(accountName, next) { 39 | var self = this; 40 | accountService.createAccount(accountName) 41 | .then(function(createdAccount) { 42 | self.ctx.account = createdAccount; 43 | next(); 44 | }); 45 | }) 46 | 47 | .when('I change the account name to "$accountName"', function(accountName, next) { 48 | var self = this; 49 | this.ctx.account.name = accountName; 50 | accountService.updateAccount(this.ctx.account) 51 | .then(function(receivedAccount) { 52 | self.ctx.account = receivedAccount; 53 | next(); 54 | }); 55 | }) 56 | 57 | .when('I delete the account', function(next) { 58 | accountService.deleteAccount(this.ctx.account.id) 59 | .then(function() { 60 | next(); 61 | }); 62 | }) 63 | 64 | .then('the account should not exist', function(next) { 65 | expect(accountService.getAccount(this.ctx.account.id)) 66 | .to.eventually.be.rejected; 67 | next(); 68 | }) 69 | 70 | .given('the following accounts\n$list', function(accountNames, next) { 71 | var self = this; 72 | accountService.createAccounts(accountNames) 73 | .then(function(createdAccounts) { 74 | self.ctx.accounts = createdAccounts; 75 | next(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/categories.steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Yadda = require('yadda'); 4 | var expect = require('./common/chai-helpers').expect; 5 | var categoryService = require('./services/category.service'); 6 | 7 | var English = Yadda.localisation.English; 8 | var Dictionary = Yadda.Dictionary; 9 | 10 | var dictionary = new Dictionary() 11 | .define('list', /([^\u0000]*)/, Yadda.converters.list); 12 | 13 | module.exports = English.library(dictionary) 14 | 15 | .when('I create a category called "$categoryName"', function(categoryName, next) { 16 | var self = this; 17 | categoryService.createCategory(categoryName) 18 | .then(function(createdCategory) { 19 | self.ctx.category = createdCategory; 20 | next(); 21 | }); 22 | }) 23 | 24 | .when('I ask for the category', function(next) { 25 | var self = this; 26 | categoryService.getCategory(this.ctx.category.id) 27 | .then(function(receivedCategory) { 28 | self.ctx.category = receivedCategory; 29 | next(); 30 | }); 31 | }) 32 | 33 | .then('I should get the category called "$categoryName"', function(categoryName, next) { 34 | expect(this.ctx.category.name).to.equal(categoryName); 35 | next(); 36 | }) 37 | 38 | .given('a category called "$categoryName"', function(categoryName, next) { 39 | var self = this; 40 | categoryService.createCategory(categoryName) 41 | .then(function(createdCategory) { 42 | self.ctx.category = createdCategory; 43 | next(); 44 | }); 45 | }) 46 | 47 | .when('I change the category name to "$categoryName"', function(categoryName, next) { 48 | var self = this; 49 | this.ctx.category.name = categoryName; 50 | categoryService.updateCategory(this.ctx.category) 51 | .then(function(receivedCategory) { 52 | self.ctx.category = receivedCategory; 53 | next(); 54 | }); 55 | }) 56 | 57 | .when('I delete the category', function(next) { 58 | categoryService.deleteCategory(this.ctx.category.id) 59 | .then(function() { 60 | next(); 61 | }); 62 | }) 63 | 64 | .then('the category should not exist', function(next) { 65 | expect(categoryService.getCategory(this.ctx.category.id)) 66 | .to.eventually.be.rejected; 67 | next(); 68 | }) 69 | 70 | .given('the following categories\n$list', function(categoryNames, next) { 71 | var self = this; 72 | categoryService.createCategories(categoryNames) 73 | .then(function(createdCategories) { 74 | self.ctx.categories = createdCategories; 75 | next(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/common/chai-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | 5 | // Extend Chai with assertions about promises 6 | chai.use(require('chai-as-promised')); 7 | 8 | // Exports 9 | module.exports = { 10 | expect: chai.expect, 11 | should: chai.should() 12 | }; 13 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/services/account.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | createAccount: createAccount, 5 | createAccounts: createAccounts, 6 | updateAccount: updateAccount, 7 | getAccount: getAccount, 8 | deleteAccount: deleteAccount 9 | }; 10 | 11 | var _ = require('lodash'); 12 | var Promise = require('bluebird'); 13 | var request = require('./request'); 14 | 15 | /** 16 | * Creates a new account. 17 | * 18 | * @param {string} name 19 | * @returns {Promise} A promise that returns the created account 20 | */ 21 | function createAccount(name) { 22 | 23 | return request.post('/accounts') 24 | .send({ 25 | name: name 26 | }) 27 | .expect(200) 28 | .then(function(res) { 29 | return res.body; 30 | }); 31 | } 32 | 33 | /** 34 | * Creates accounts. 35 | * 36 | * @param {string[]} accountNames 37 | * @returns {Promise} A promise that returns the created accounts 38 | */ 39 | function createAccounts(names) { 40 | 41 | var tasks = []; 42 | _.each(names, function(name) { 43 | tasks.push(request.post('/accounts').send({name: name}).expect(200)); 44 | }); 45 | 46 | return Promise.all(tasks) 47 | .then(function(responses) { 48 | return _.map(responses, function(response) { 49 | return response.body; 50 | }); 51 | }); 52 | } 53 | 54 | /** 55 | * Updates an existing account. 56 | * 57 | * @param {string} name 58 | * @returns {Promise} A promise that returns the created account 59 | */ 60 | function updateAccount(account) { 61 | 62 | return request.put('/accounts/' + account.id) 63 | .send(account) 64 | .expect(200) 65 | .then(function(res) { 66 | return res.body; 67 | }); 68 | } 69 | 70 | /** 71 | * Gets an account. 72 | * 73 | * @param {number} id 74 | * @returns {Promise} A promise that returns the desired account 75 | * @throws {String} "Not found" if account is not found 76 | */ 77 | function getAccount(id) { 78 | 79 | return request.get('/accounts/' + id) 80 | .expect(200) 81 | .then(function(res) { 82 | return res.body; 83 | }); 84 | } 85 | 86 | /** 87 | * Deletes an account. 88 | * 89 | * @param {number} id 90 | * @returns {Promise} A promise that returns true when the account is deleted 91 | */ 92 | function deleteAccount(id) { 93 | 94 | return request.delete('/accounts/' + id) 95 | .expect(204) 96 | .then(function() { 97 | return true; 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/services/category.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | createCategory: createCategory, 5 | createCategories: createCategories, 6 | updateCategory: updateCategory, 7 | getCategory: getCategory, 8 | deleteCategory: deleteCategory 9 | }; 10 | 11 | var _ = require('lodash'); 12 | var Promise = require('bluebird'); 13 | var request = require('./request'); 14 | 15 | /** 16 | * Creates a new category. 17 | * 18 | * @param {string} name 19 | * @returns {Promise} A promise that returns the created category 20 | */ 21 | function createCategory(name) { 22 | 23 | return request.post('/categories') 24 | .send({ 25 | name: name 26 | }) 27 | .expect(200) 28 | .then(function(res) { 29 | return res.body; 30 | }); 31 | } 32 | 33 | /** 34 | * Creates categories. 35 | * 36 | * @param {string[]} categoryNames 37 | * @returns {Promise} A promise that returns the created categories 38 | */ 39 | function createCategories(names) { 40 | 41 | var tasks = []; 42 | _.each(names, function(name) { 43 | tasks.push(request.post('/categories').send({name: name}).expect(200)); 44 | }); 45 | 46 | return Promise.all(tasks) 47 | .then(function(responses) { 48 | return _.map(responses, function(response) { 49 | return response.body; 50 | }); 51 | }); 52 | } 53 | 54 | /** 55 | * Updates an existing category. 56 | * 57 | * @param {string} name 58 | * @returns {Promise} A promise that returns the created category 59 | */ 60 | function updateCategory(category) { 61 | 62 | return request.put('/categories/' + category.id) 63 | .send(category) 64 | .expect(200) 65 | .then(function(res) { 66 | return res.body; 67 | }); 68 | } 69 | 70 | /** 71 | * Gets a category. 72 | * 73 | * @param {number} id 74 | * @returns {Promise} A promise that returns the desired category 75 | * @throws {String} "Not found" if category is not found 76 | */ 77 | function getCategory(id) { 78 | 79 | return request.get('/categories/' + id) 80 | .expect(200) 81 | .then(function(res) { 82 | return res.body; 83 | }); 84 | } 85 | 86 | /** 87 | * Deletes a category. 88 | * 89 | * @param {number} id 90 | * @returns {Promise} A promise that returns true when the category is deleted 91 | */ 92 | function deleteCategory(id) { 93 | 94 | return request.delete('/categories/' + id) 95 | .expect(204) 96 | .then(function() { 97 | return true; 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/services/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('supertest-as-promised'); 4 | 5 | request = request('http://localhost:8080'); 6 | 7 | module.exports = request; 8 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/services/transaction.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | createTransaction: createTransaction, 5 | createTransactions: createTransactions, 6 | updateTransaction: updateTransaction, 7 | getTransaction: getTransaction, 8 | getTransactionsByCategory: getTransactionsByCategory, 9 | deleteTransaction: deleteTransaction 10 | }; 11 | 12 | var _ = require('lodash'); 13 | var Promise = require('bluebird'); 14 | var request = require('./request'); 15 | 16 | /** 17 | * Creates a new transaction. 18 | * 19 | * @param {Transaction} transaction 20 | * @returns {Promise} A promise that returns the created transaction 21 | */ 22 | function createTransaction(transaction) { 23 | 24 | return request.post('/transactions') 25 | .send(transaction) 26 | .expect(200) 27 | .then(function(res) { 28 | return res.body; 29 | }); 30 | } 31 | 32 | /** 33 | * Creates transactions. 34 | * 35 | * @param {Transaction[]} transactions 36 | * @returns {Promise} A promise that returns the created transactions 37 | */ 38 | function createTransactions(transactions) { 39 | 40 | var tasks = []; 41 | _.each(transactions, function(transaction) { 42 | tasks.push(request.post('/transactions').send(transaction).expect(200)); 43 | }); 44 | 45 | return Promise.all(tasks) 46 | .then(function(responses) { 47 | return _.map(responses, function(response) { 48 | return response.body; 49 | }); 50 | }); 51 | } 52 | 53 | /** 54 | * Updates an existing transaction. 55 | * 56 | * @param {string} name 57 | * @returns {Promise} A promise that returns the created transaction 58 | */ 59 | function updateTransaction(transaction) { 60 | 61 | return request.put('/transactions/' + transaction.id) 62 | .send(transaction) 63 | .expect(200) 64 | .then(function(res) { 65 | return res.body; 66 | }); 67 | } 68 | 69 | /** 70 | * Gets a transaction. 71 | * 72 | * @param {number} id 73 | * @returns {Promise} A promise that returns the desired transaction 74 | * @throws {String} "Not found" if transaction is not found 75 | */ 76 | function getTransaction(id) { 77 | 78 | return request.get('/transactions/' + id) 79 | .expect(200) 80 | .then(function(res) { 81 | return res.body; 82 | }); 83 | } 84 | 85 | /** 86 | * Gets transactions by category. 87 | * 88 | * @param startDate 89 | * @param endDate 90 | * @returns {Promise} A promise that returns categories with total amounts. 91 | */ 92 | function getTransactionsByCategory(startDate, endDate) { 93 | 94 | var dateRange = '&startDate=' + startDate.toISOString() + '&endDate=' + endDate.toISOString(); 95 | 96 | return request.get('/transactions?groupByCategory' + dateRange) 97 | .expect(200) 98 | .then(function(response) { 99 | return response.body; 100 | }); 101 | } 102 | 103 | /** 104 | * Deletes a transaction. 105 | * 106 | * @param {number} id 107 | * @returns {Promise} A promise that returns true when the transaction is deleted 108 | */ 109 | function deleteTransaction(id) { 110 | 111 | return request.delete('/transactions/' + id) 112 | .expect(204) 113 | .then(function() { 114 | return true; 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/steps/transactions.steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Yadda = require('yadda'); 5 | var expect = require('./common/chai-helpers').expect; 6 | var transactionService = require('./services/transaction.service'); 7 | 8 | var English = Yadda.localisation.English; 9 | var Dictionary = Yadda.Dictionary; 10 | 11 | var dictionary = new Dictionary() 12 | .define('table', /([^\u0000]*)/, Yadda.converters.table); 13 | 14 | function specToResource(txnSpec, accountId, categoryId) { 15 | return { 16 | txn_date: (txnSpec.date) ? new Date(txnSpec.date) : new Date(), 17 | payee: txnSpec.payee, 18 | memo: txnSpec.memo, 19 | amount: parseFloat(txnSpec.amount), 20 | account_id: accountId, 21 | category_id: categoryId 22 | }; 23 | } 24 | 25 | function receivedToResource(received) { 26 | return { 27 | id: received.id, 28 | txn_date: new Date(received.txn_date), 29 | payee: received.payee, 30 | memo: received.memo, 31 | amount: parseFloat(received.amount), 32 | account_id: received.account.id, 33 | category_id: received.category.id 34 | }; 35 | } 36 | 37 | function receivedToResources(receivedList) { 38 | var txns = []; 39 | _.each(receivedList, function(received) { 40 | txns.push(receivedToResource(received)); 41 | }); 42 | return txns; 43 | } 44 | 45 | module.exports = English.library(dictionary) 46 | 47 | .when('I make the following deposit to $account\n$table', function(account, table, next) { 48 | var self = this; 49 | var txnSpec = table[0]; 50 | var accountId = _.findWhere( self.ctx.accounts, {name: account} ).id; 51 | var categoryId = _.findWhere( self.ctx.categories, {name: txnSpec.category} ).id; 52 | var txn = specToResource(txnSpec, accountId, categoryId); 53 | transactionService.createTransaction(txn) 54 | .then(function(createdTransaction) { 55 | self.ctx.transaction = receivedToResource(createdTransaction); 56 | next(); 57 | }); 58 | }) 59 | 60 | .when('I make the following payment from $account\n$table', function(account, table, next) { 61 | var self = this; 62 | var txnSpec = table[0]; 63 | txnSpec.amount = -txnSpec.amount; // payments are negative 64 | var accountId = _.findWhere( self.ctx.accounts, {name: account} ).id; 65 | var categoryId = _.findWhere( self.ctx.categories, {name: txnSpec.category} ).id; 66 | var txn = specToResource(txnSpec, accountId, categoryId); 67 | transactionService.createTransaction(txn) 68 | .then(function(createdTransaction) { 69 | self.ctx.transaction = receivedToResource(createdTransaction); 70 | next(); 71 | }); 72 | }) 73 | 74 | .when('I make the following purchase using $account\n$table', function(account, table, next) { 75 | var self = this; 76 | var txnSpec = table[0]; 77 | txnSpec.amount = -txnSpec.amount; // payments are negative 78 | var accountId = _.findWhere( self.ctx.accounts, {name: account} ).id; 79 | var categoryId = _.findWhere( self.ctx.categories, {name: txnSpec.category} ).id; 80 | var txn = specToResource(txnSpec, accountId, categoryId); 81 | transactionService.createTransaction(txn) 82 | .then(function(createdTransaction) { 83 | self.ctx.transaction = receivedToResource(createdTransaction); 84 | next(); 85 | }); 86 | }) 87 | 88 | .when('I ask for the transaction', function(next) { 89 | var self = this; 90 | transactionService.getTransaction(this.ctx.transaction.id) 91 | .then(function(receivedTransaction) { 92 | self.ctx.transaction = receivedToResource(receivedTransaction); 93 | next(); 94 | }); 95 | }) 96 | 97 | .then('I should get the following deposit\n$table', function(table, next) { 98 | var self = this; 99 | var actual = this.ctx.transaction; 100 | var txnSpec = table[0]; 101 | 102 | // Prepare fields of expected transaction 103 | var categoryId = _.findWhere( self.ctx.categories, {name: txnSpec.category} ).id; 104 | var expected = specToResource(txnSpec, actual.account_id, categoryId); 105 | expected.id = actual.id; 106 | expected.txn_date = actual.txn_date; 107 | 108 | expect(actual).to.deep.equal(expected); 109 | next(); 110 | }) 111 | 112 | .then('I should get the following payment\n$table', function(table, next) { 113 | var self = this; 114 | var actual = this.ctx.transaction; 115 | var txnSpec = table[0]; 116 | 117 | // Prepare fields of expected transaction 118 | txnSpec.amount = -txnSpec.amount; // payments are negative 119 | var categoryId = _.findWhere( self.ctx.categories, {name: txnSpec.category} ).id; 120 | var expected = specToResource(txnSpec, actual.account_id, categoryId); 121 | expected.id = actual.id; 122 | expected.txn_date = actual.txn_date; 123 | 124 | expect(actual).to.deep.equal(expected); 125 | next(); 126 | }) 127 | 128 | .given('a transaction with the following properties\n$table', function(table, next) { 129 | var self = this; 130 | var txnSpec = table[0]; 131 | var accountId = _.findWhere( self.ctx.accounts, {name: txnSpec.account} ).id; 132 | var categoryId = _.findWhere( self.ctx.categories, {name: txnSpec.category} ).id; 133 | var txn = specToResource(txnSpec, accountId, categoryId); 134 | transactionService.createTransaction(txn) 135 | .then(function(createdTransaction) { 136 | self.ctx.transaction = receivedToResource(createdTransaction); 137 | next(); 138 | }); 139 | }) 140 | 141 | .when('I change the purchase amount to $amount', function(amount, next) { 142 | var self = this; 143 | this.ctx.transaction.amount = -amount; // payments are negative 144 | transactionService.updateTransaction(this.ctx.transaction) 145 | .then(function(receivedTransaction) { 146 | self.ctx.transaction = receivedToResource(receivedTransaction); 147 | next(); 148 | }); 149 | }) 150 | 151 | .when('I delete the transaction', function(next) { 152 | transactionService.deleteTransaction(this.ctx.transaction.id) 153 | .then(function() { 154 | next(); 155 | }); 156 | }) 157 | 158 | .then('the transaction should not exist', function(next) { 159 | expect(transactionService.getTransaction(this.ctx.transaction.id)) 160 | .to.eventually.be.rejected; 161 | next(); 162 | }) 163 | 164 | .given('the following transactions\n$table', function(txnSpecs, next) { 165 | var self = this; 166 | 167 | // Convert transaction specs to resources 168 | var txns = []; 169 | _.each(txnSpecs, function(txnSpec) { 170 | var accountId = _.findWhere( self.ctx.accounts, {name: txnSpec.account} ).id; 171 | var categoryId = _.findWhere( self.ctx.categories, {name: txnSpec.category} ).id; 172 | txns.push(specToResource(txnSpec, accountId, categoryId)); 173 | }); 174 | 175 | // Create transactions 176 | transactionService.createTransactions(txns) 177 | .then(function(createdTransactions) { 178 | self.ctx.transactions = receivedToResources(createdTransactions); 179 | next(); 180 | }); 181 | }) 182 | 183 | .when('I ask for transactions by category with start date of $start and end date of $end', 184 | function(start, end, next) { 185 | var self = this; 186 | transactionService.getTransactionsByCategory(new Date(start), new Date(end)) 187 | .then(function(transactionsByCategory) { 188 | self.ctx.transactionsByCategory = transactionsByCategory; 189 | next(); 190 | }); 191 | }) 192 | 193 | .then('I should get the following summary of transactions by category\n$table', function(expectedItems, next) { 194 | var self = this; 195 | _.each(expectedItems, function(expectedItem) { 196 | var actualItem = _.findWhere( self.ctx.transactionsByCategory, {cat_name: expectedItem.category} ); 197 | expect(actualItem.amount).to.equal(parseFloat(expectedItem.amount)); 198 | }); 199 | next(); 200 | }); 201 | -------------------------------------------------------------------------------- /mocha-yadda-supertest/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Yadda = require('yadda'); 4 | Yadda.plugins.mocha.StepLevelPlugin.init(); 5 | 6 | var knex = null; 7 | 8 | // Establish before hook - this will run before any feature file is executed 9 | before(initDb); 10 | 11 | // Establish after hook - this will run after all feature files have been executed 12 | after(releaseDb); 13 | 14 | new Yadda.FeatureFileSearch('./test/features').each(function(file) { 15 | 16 | featureFile(file, function(feature) { 17 | 18 | var libraries = require_feature_libraries(feature); 19 | var yadda = Yadda.createInstance(libraries); 20 | 21 | scenarios(feature.scenarios, function(scenario) { 22 | 23 | // Truncate tables before every scenario 24 | before(truncateTables); 25 | 26 | var ctx = {}; 27 | steps(scenario.steps, function(step, done) { 28 | yadda.run(step, { ctx: ctx }, done); 29 | }); 30 | }); 31 | }); 32 | }); 33 | 34 | function require_feature_libraries(feature) { 35 | return feature.annotations.libraries.split(', ').reduce(require_library, []); 36 | } 37 | 38 | function require_library(libraries, library) { 39 | return libraries.concat(require('./steps/' + library + '.steps')); 40 | } 41 | 42 | function initDb(done) { 43 | knex = require('knex')({ 44 | client: 'postgresql', 45 | debug: false, 46 | connection: { 47 | host: 'localhost', 48 | user: '', 49 | password: '', 50 | database: 'manage-my-money', 51 | charset: 'utf8' 52 | } 53 | }); 54 | done(); 55 | } 56 | 57 | function releaseDb(done) { 58 | if (knex && knex.client) { 59 | return knex.destroy() 60 | .then(function() { 61 | done(); 62 | }); 63 | } 64 | } 65 | 66 | function truncateTables(done) { 67 | return knex.raw('truncate table accounts, categories, transactions cascade') 68 | .then(function() { 69 | done(); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/README.md: -------------------------------------------------------------------------------- 1 | wdio-mocha-webdriverio 2 | ====================== 3 | ``` 4 | Test runner: wdio 5 | Testing framework: mocha 6 | Front-end driver: WebdriverIO 7 | ``` 8 | 9 | Running tests 10 | ------------- 11 | 12 | - Make sure that mocha and webdriverio are installed globally 13 | 14 | ```bash 15 | $ npm install -g mocha webdriverio 16 | ``` 17 | 18 | - Make sure *Manage My Money* application is running 19 | 20 | ```bash 21 | # --- In command shell 1 --- 22 | $ cd Projects/manage-my-money-server 23 | $ npm start 24 | 25 | # --- In command shell 2 --- 26 | $ cd Projects/manage-my-money-client 27 | $ gulp serve-dev 28 | ``` 29 | 30 | - Run the Selenium Standalone Server: 31 | 32 | ```bash 33 | # --- In command shell 3 --- 34 | $ java -jar /usr/local/bin/selenium-server-standalone-2.47.1.jar 35 | ``` 36 | 37 | - Run the tests: 38 | 39 | ```bash 40 | # --- In command shell 4 --- 41 | $ cd 42 | $ npm install 43 | $ wdio wdio.conf.js 44 | ``` 45 | 46 | By default, the tests run in Chrome. To switch to another browser, change the `browserName` in `wdio.conf.js`. 47 | 48 | Skipping specific tests 49 | ----------------------- 50 | TODO 51 | 52 | Running only specific tests 53 | --------------------------- 54 | TODO 55 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wdio-mocha-webdriverio", 3 | "description": "wdio-mocha-webdriverio", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/archfirst/acceptance-testing-best-practices.git" 8 | }, 9 | "devDependencies": { 10 | "chai": "^3.0.0", 11 | "chai-as-promised": "^5.1.0", 12 | "chai-datetime": "^1.4.0", 13 | "chai-stats": "^0.3.0", 14 | "knex": "^0.8.6", 15 | "pg": "^4.4.0", 16 | "webdriverio": "^3.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/pages/accounts.page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AccountsPage = function() { 4 | this.url = '/settings/accounts'; 5 | }; 6 | 7 | AccountsPage.prototype.createAccount = function(name) { 8 | return browser 9 | .url(this.url) 10 | .setValue('.accountAddForm-name', name) 11 | .click('.accountAddForm button'); 12 | }; 13 | 14 | AccountsPage.prototype.changeAccountName = function *(curretName, newName) { 15 | 16 | // Get the account row 17 | var accountRow = yield browser 18 | .url(this.url) 19 | .element('.accountView=' + curretName) 20 | .element('..'); 21 | 22 | // Click on the account row to show the account form 23 | var accountForm = yield browser 24 | .elementIdClick(accountRow.value.ELEMENT) 25 | .elementIdElement(accountRow.value.ELEMENT, '.accountForm'); 26 | 27 | // Fill in the account name 28 | var nameField = yield browser 29 | .elementIdElement(accountForm.value.ELEMENT, '.accountForm-name'); 30 | 31 | yield browser 32 | .elementIdClear(nameField.value.ELEMENT) 33 | .elementIdValue(nameField.value.ELEMENT, newName); 34 | 35 | // Submit the form 36 | yield browser 37 | .submit(accountForm.value.ELEMENT); 38 | }; 39 | 40 | AccountsPage.prototype.doesAccountExist = function(name) { 41 | return browser 42 | .url(this.url) 43 | .isExisting('.accountView=' + name); 44 | }; 45 | 46 | module.exports = AccountsPage; 47 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/pages/categories.page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CategoriesPage = function() { 4 | this.url = '/settings/categories'; 5 | }; 6 | 7 | CategoriesPage.prototype.createCategory = function(name) { 8 | return browser 9 | .url(this.url) 10 | .setValue('.categoryAddForm-name', name) 11 | .click('.categoryAddForm button'); 12 | }; 13 | 14 | CategoriesPage.prototype.changeCategoryName = function *(curretName, newName) { 15 | 16 | // Get the category row 17 | var categoryRow = yield browser 18 | .url(this.url) 19 | .element('.categoryView=' + curretName) 20 | .element('..'); 21 | 22 | // Click on the category row to show the category form 23 | var categoryForm = yield browser 24 | .elementIdClick(categoryRow.value.ELEMENT) 25 | .elementIdElement(categoryRow.value.ELEMENT, '.categoryForm'); 26 | 27 | // Fill in the category name 28 | var nameField = yield browser 29 | .elementIdElement(categoryForm.value.ELEMENT, '.categoryForm-name'); 30 | 31 | yield browser 32 | .elementIdClear(nameField.value.ELEMENT) 33 | .elementIdValue(nameField.value.ELEMENT, newName); 34 | 35 | // Submit the form 36 | yield browser 37 | .submit(categoryForm.value.ELEMENT); 38 | }; 39 | 40 | CategoriesPage.prototype.doesCategoryExist = function(name) { 41 | return browser 42 | .url(this.url) 43 | .isExisting('.categoryView=' + name); 44 | }; 45 | 46 | module.exports = CategoriesPage; 47 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/pages/dashboard.page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DashboardPage = function() { 4 | this.url = '/'; 5 | }; 6 | 7 | DashboardPage.prototype.getNumberOfCategories = function() { 8 | return browser 9 | .url(this.url) 10 | .selectByVisibleText('.txnFilterForm-period', 'All time') 11 | .elements('.chart .plot .bar') 12 | .then(function(res) { 13 | return res.value.length; 14 | }) 15 | }; 16 | 17 | module.exports = DashboardPage; 18 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/pages/transactions.page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var TransactionsPage = function() { 4 | this.url = '/accounts'; 5 | }; 6 | 7 | /** 8 | * Create a transaction 9 | * @param {Object} txn 10 | * Example: 11 | * { 12 | * account: 'Amazon VISA', 13 | * date: '02/01/2015', 14 | * payee: 'Chevron Gas Station', 15 | * memo: 'Gas', 16 | * category: 'Auto & Transport', 17 | * payment: '30.00', 18 | * deposit: '' 19 | * } 20 | * @returns {Promise} 21 | */ 22 | TransactionsPage.prototype.createTransaction = function(transaction) { 23 | 24 | var amount = (transaction.payment.length > 0) ? transaction.payment : transaction.deposit; 25 | var amountElement = (transaction.payment.length > 0) ? 'payment' : 'deposit'; 26 | var txn_date = transaction.date.replace(/\//g, ''); // remove slashes 27 | 28 | return browser 29 | .url(this.url) 30 | .element('.accountsPanel ul').click('a=' + transaction.account) 31 | .waitForExist('.transactionsPanel') 32 | .click('.transactionsPanel button') 33 | .pause(300) // wait for dialog box animation to complete 34 | .element('form[name=transactionForm]').click('#txn_date').keys(txn_date) 35 | .setValue('form[name=transactionForm] #payee', transaction.payee) 36 | .setValue('form[name=transactionForm] #memo', transaction.memo) 37 | .selectByVisibleText('form[name=transactionForm] #category', transaction.category) 38 | .setValue('form[name=transactionForm] #' + amountElement, amount) 39 | .element('form[name=transactionForm]').click('button=OK'); 40 | }; 41 | 42 | TransactionsPage.prototype.changePaymentAmount = function(account, payee, newAmount) { 43 | return browser 44 | .url(this.url) 45 | .element('.accountsPanel ul').click('a=' + account) 46 | .waitForExist('.transactionsPanel') 47 | .element('.transactions-columnPayee=' + payee).element('..') 48 | .then(function(res) { 49 | return browser.elementIdClick(res.value.ELEMENT); 50 | }) 51 | .pause(300) // wait for dialog box animation to complete 52 | .then(function() { 53 | return browser.setValue('form[name=transactionForm] #payment', newAmount); 54 | }) 55 | .then(function() { 56 | return browser.element('form[name=transactionForm]').click('button=OK'); 57 | }); 58 | }; 59 | 60 | TransactionsPage.prototype.getTransactionForPayee = function *(account, payee) { 61 | 62 | // Get the transaction row with the specified payee 63 | var rowWebElement = yield browser 64 | .url(this.url) 65 | .element('.accountsPanel ul').click('a=' + account) 66 | .waitForExist('.transactionsPanel') 67 | .element('.transactions-columnPayee=' + payee) 68 | .element('..'); 69 | var row = rowWebElement.value.ELEMENT; 70 | 71 | var date = yield browser 72 | .elementIdElement(row, '.transactions-columnDate').getText(); 73 | 74 | var memo = yield browser 75 | .elementIdElement(row, '.transactions-columnMemo').getText(); 76 | 77 | var category = yield browser 78 | .elementIdElement(row, '.transactions-columnCategory').getText(); 79 | 80 | var payment = yield browser 81 | .elementIdElement(row, '.transactions-columnPayment').getText(); 82 | 83 | var deposit = yield browser 84 | .elementIdElement(row, '.transactions-columnDeposit').getText(); 85 | 86 | return { 87 | account: account, 88 | date: date, 89 | payee: payee, 90 | memo: memo, 91 | category: category, 92 | payment: payment, 93 | deposit: deposit 94 | }; 95 | }; 96 | 97 | TransactionsPage.prototype.deleteTransaction = function *(account, payee) { 98 | return browser 99 | .url(this.url) 100 | .element('.accountsPanel ul').click('a=' + account) 101 | .waitForExist('.transactionsPanel') 102 | .element('.transactions-columnPayee=' + payee).element('..') 103 | .then(function(res) { 104 | return browser.elementIdClick(res.value.ELEMENT); 105 | }) 106 | .pause(300) // wait for dialog box animation to complete 107 | .then(function() { 108 | return browser.element('form[name=transactionForm]').click('button=Delete'); 109 | }); 110 | }; 111 | 112 | TransactionsPage.prototype.doesTransactionExist = function(account, payee) { 113 | return browser 114 | .url(this.url) 115 | .element('.accountsPanel ul').click('a=' + account) 116 | .waitForExist('.transactionsPanel') 117 | .isExisting('.transactions-columnPayee=' + payee); 118 | }; 119 | 120 | module.exports = TransactionsPage; 121 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/specs/accounts.page.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('./chai-helpers').expect; 4 | var AccountsPage = require('../pages/accounts.page'); 5 | 6 | describe('Accounts page', function() { 7 | 8 | var page; 9 | 10 | beforeEach(function () { 11 | page = new AccountsPage(); 12 | }); 13 | 14 | it('should allow the creation of an account', function *() { 15 | // Given an account called "Cash" 16 | yield page.createAccount('Cash'); 17 | 18 | // Then the accounts page should show an account called "Cash" 19 | var exists = yield page.doesAccountExist('Cash'); 20 | expect(exists).to.be.true; 21 | }); 22 | 23 | it('should allow changing the name of an account', function *() { 24 | // Given an account called "BofA Checking" 25 | yield page.createAccount('BofA Checking'); 26 | 27 | // When I change the account name to "Bank of America Checking" 28 | yield page.changeAccountName('BofA Checking', 'Bank of America Checking'); 29 | 30 | // Then the accounts page should show an account called "Bank of America Checking" 31 | var exists = yield page.doesAccountExist('Bank of America Checking'); 32 | expect(exists).to.be.true; 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/specs/categories.page.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('./chai-helpers').expect; 4 | var CategoriesPage = require('../pages/categories.page'); 5 | 6 | describe('Categories page', function() { 7 | 8 | var page; 9 | 10 | beforeEach(function () { 11 | page = new CategoriesPage(); 12 | }); 13 | 14 | it('should allow the creation of a category', function *() { 15 | // Given a category called "Shopping" 16 | yield page.createCategory('Shopping'); 17 | 18 | // Then the categories page should show a category called "Shopping" 19 | var exists = yield page.doesCategoryExist('Shopping'); 20 | expect(exists).to.be.true; 21 | }); 22 | 23 | it('should allow changing the name of a category', function *() { 24 | // Given a category called "Shopping" 25 | yield page.createCategory('Shopping'); 26 | 27 | // When I change the category name to "General Shopping" 28 | yield page.changeCategoryName('Shopping', 'General Shopping'); 29 | 30 | // Then the categories page should show a category called "General Shopping" 31 | var exists = yield page.doesCategoryExist('General Shopping'); 32 | expect(exists).to.be.true; 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/specs/chai-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | 5 | // Extend Chai with assertions about promises 6 | chai.use(require('chai-as-promised')); 7 | 8 | // Matchers for common date comparisons 9 | chai.use(require('chai-datetime')); 10 | 11 | // Statistical and numerical assertions such as .almost.equal 12 | chai.use(require('chai-stats')); 13 | 14 | // Exports 15 | module.exports = { 16 | expect: chai.expect, 17 | should: chai.should() 18 | }; 19 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/specs/dashboard.page.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('./chai-helpers').expect; 4 | var AccountsPage = require('../pages/accounts.page'); 5 | var CategoriesPage = require('../pages/categories.page'); 6 | var DashboardPage = require('../pages/dashboard.page'); 7 | var TransactionsPage = require('../pages/transactions.page'); 8 | 9 | describe('Dashboard page', function() { 10 | 11 | var page; 12 | 13 | beforeEach(function *() { 14 | page = new DashboardPage(); 15 | 16 | /* ----- Background ----- */ 17 | // Given the following accounts 18 | var accountsPage = new AccountsPage(); 19 | yield accountsPage.createAccount('Cash'); 20 | yield accountsPage.createAccount('Credit'); 21 | 22 | // And the following categories 23 | var categoriesPage = new CategoriesPage(); 24 | yield categoriesPage.createCategory('Auto & Transport'); 25 | yield categoriesPage.createCategory('Food & Dining'); 26 | 27 | // And the following transactions 28 | var transactionsPage = new TransactionsPage(); 29 | yield transactionsPage.createTransaction({ 30 | account: 'Cash', date: '01/01/2015', payee: 'Gas Station', memo: 'Gas', category: 'Auto & Transport', payment: '10.00', deposit: '' 31 | }); 32 | yield transactionsPage.createTransaction({ 33 | account: 'Credit', date: '01/02/2015', payee: 'Grocery Store', memo: 'Food', category: 'Food & Dining', payment: '20.00', deposit: '' 34 | }); 35 | yield transactionsPage.createTransaction({ 36 | account: 'Cash', date: '01/03/2015', payee: 'Gas Station', memo: 'Gas', category: 'Auto & Transport', payment: '30.00', deposit: '' 37 | }); 38 | yield transactionsPage.createTransaction({ 39 | account: 'Credit', date: '01/04/2015', payee: 'Grocery Store', memo: 'Food', category: 'Food & Dining', payment: '40.00', deposit: '' 40 | }); 41 | yield transactionsPage.createTransaction({ 42 | account: 'Cash', date: '01/05/2015', payee: 'Gas Station', memo: 'Gas', category: 'Auto & Transport', payment: '50.00', deposit: '' 43 | }); 44 | yield transactionsPage.createTransaction({ 45 | account: 'Credit', date: '01/06/2015', payee: 'Grocery Store', memo: 'Food', category: 'Food & Dining', payment: '60.00', deposit: '' 46 | }); 47 | }); 48 | 49 | it('should show a summary of transactions by category', function *() { 50 | // Expected category totals 51 | // | category | payment | 52 | // | Auto & Transport | 90.00 | 53 | // | Food & Dining | 120.00 | 54 | 55 | // We will simply check for the number of horizontal bars 56 | var numberOfCategories = yield page.getNumberOfCategories(); 57 | expect(numberOfCategories).to.equal(2); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/specs/hooks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var knex = null; 4 | 5 | // Runs before all test cases. 6 | // Initializes knex. 7 | before(function() { 8 | knex = require('knex')({ 9 | client: 'postgresql', 10 | debug: false, 11 | connection: { 12 | host: 'localhost', 13 | user: '', 14 | password: '', 15 | database: 'manage-my-money', 16 | charset: 'utf8' 17 | } 18 | }); 19 | }); 20 | 21 | // Runs before each test case. 22 | // Truncates tables. 23 | beforeEach(function() { 24 | return knex.raw('truncate table accounts, categories, transactions cascade'); 25 | }); 26 | 27 | // Runs after each test case. 28 | // Does nothing. 29 | afterEach(function() { 30 | }); 31 | 32 | // Runs after all test cases. 33 | // Destroys the database connection. 34 | after(function() { 35 | if (knex && knex.client) { 36 | return knex.destroy(); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/test/specs/transactions.page.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('./chai-helpers').expect; 4 | var AccountsPage = require('../pages/accounts.page'); 5 | var CategoriesPage = require('../pages/categories.page'); 6 | var TransactionsPage = require('../pages/transactions.page'); 7 | 8 | describe('Transactions page', function() { 9 | 10 | var page; 11 | 12 | beforeEach(function *() { 13 | page = new TransactionsPage(); 14 | 15 | /* ----- Background ----- */ 16 | // Given an account called "Amazon VISA" 17 | var accountsPage = new AccountsPage(); 18 | yield accountsPage.createAccount('Amazon VISA'); 19 | 20 | // And a category called "Auto & Transport" 21 | var categoriesPage = new CategoriesPage(); 22 | yield categoriesPage.createCategory('Auto & Transport'); 23 | }); 24 | 25 | it('should allow the creation of a transaction', function *() { 26 | // Given a transaction with the following properties 27 | var transaction = { 28 | account: 'Amazon VISA', 29 | date: '02/01/2015', 30 | payee: 'Chevron Gas Station', 31 | memo: 'Gas', 32 | category: 'Auto & Transport', 33 | payment: '30.00', 34 | deposit: '' 35 | }; 36 | yield page.createTransaction(transaction); 37 | 38 | // Then the transactions page should show the transaction 39 | var result = yield page.getTransactionForPayee(transaction.account, transaction.payee); 40 | expect(result).to.deep.equal(transaction); 41 | }); 42 | 43 | it('should allow changing the payment amount of a transaction', function *() { 44 | // Given a transaction with the following properties 45 | var transaction = { 46 | account: 'Amazon VISA', 47 | date: '02/01/2015', 48 | payee: 'Chevron Gas Station', 49 | memo: 'Gas', 50 | category: 'Auto & Transport', 51 | payment: '30.00', 52 | deposit: '' 53 | }; 54 | yield page.createTransaction(transaction); 55 | 56 | // When I change the payment amount to 50.00 57 | transaction.payment = '50.00'; 58 | yield page.changePaymentAmount(transaction.account, transaction.payee, transaction.payment); 59 | 60 | // Then the transactions page should show the modified transaction 61 | var result = yield page.getTransactionForPayee(transaction.account, transaction.payee); 62 | expect(result).to.deep.equal(transaction); 63 | }); 64 | 65 | it('should allow the deletion of a transaction', function *() { 66 | // Given a transaction with the following properties 67 | var transaction = { 68 | account: 'Amazon VISA', 69 | date: '02/01/2015', 70 | payee: 'Chevron Gas Station', 71 | memo: 'Gas', 72 | category: 'Auto & Transport', 73 | payment: '30.00', 74 | deposit: '' 75 | }; 76 | yield page.createTransaction(transaction); 77 | 78 | // When I delete the transaction 79 | yield page.deleteTransaction(transaction.account, transaction.payee); 80 | 81 | // Then the transaction should not exist 82 | var exists = yield page.doesTransactionExist(transaction.account, transaction.payee); 83 | expect(exists).to.be.false; 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /wdio-mocha-webdriverio/wdio.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | 3 | // 4 | // ================== 5 | // Specify Test Files 6 | // ================== 7 | // Define which test specs should run. The pattern is relative to the location of this 8 | // file. 9 | // 10 | specs: [ 11 | './test/specs/**/*.js' 12 | ], 13 | // Patterns to exclude. 14 | exclude: [ 15 | // 'path/to/excluded/files' 16 | ], 17 | // 18 | // ============ 19 | // Capabilities 20 | // ============ 21 | // Define your capabilities here. WebdriverIO can run multiple capabilties at the same 22 | // time. Depending on the number of capabilities WebdriverIO launches several test 23 | // sessions. Within your capabilities you can overwrite the spec and exclude option in 24 | // order to group specific specs to a specific capability. 25 | // 26 | // If you have trouble getting all important capabilities together check out Sauce Labs 27 | // platform configurator. A great tool to configure your capabilities. 28 | // https://docs.saucelabs.com/reference/platforms-configurator 29 | // 30 | capabilities: [{ 31 | browserName: 'chrome' 32 | }], 33 | // 34 | // =================== 35 | // Test Configurations 36 | // =================== 37 | // Define all options that are relevant for the WebdriverIO instance here 38 | // 39 | // Level of logging verbosity. 40 | logLevel: 'silent', 41 | // 42 | // Enables colors for log output 43 | coloredLogs: true, 44 | // 45 | // Saves a screenshot to a given path if a command fails. 46 | screenshotPath: './errorShots/', 47 | // 48 | // Shorten url command calls by setting a base url. If your url parameter starts with "/" 49 | // the base url gets prepended. 50 | baseUrl: 'http://localhost:3000', 51 | // 52 | // Default timeout for all waitForXXX commands. 53 | waitforTimeout: 10000, 54 | // 55 | // Initialise the browser instance with a WebdriverIO plugin. The object should have the 56 | // plugin name as key and the desired plugin options as property. Make sure you have 57 | // the plugin installed before running any tests. The following plugins are currently 58 | // available: 59 | // WebdriverCSS: https://github.com/webdriverio/webdrivercss 60 | // WebdriverRTC: https://github.com/webdriverio/webdriverrtc 61 | // Browserevent: https://github.com/webdriverio/browserevent 62 | // plugins: { 63 | // webdrivercss: { 64 | // screenshotRoot: 'my-shots', 65 | // failedComparisonsRoot: 'diffs', 66 | // misMatchTolerance: 0.05, 67 | // screenWidth: [320,480,640,1024] 68 | // }, 69 | // webdriverrtc: {}, 70 | // browserevent: {} 71 | // }, 72 | // 73 | // Framework you want to run your specs with. 74 | // The following are supported: mocha, jasmine and cucumber 75 | // see also: http://webdriver.io/guide/testrunner/frameworks.html 76 | // 77 | // Make sure you have the node package for the specific framework installed before running 78 | // any tests. If not please install the following package: 79 | // Mocha: `$ npm install mocha` 80 | // Jasmine: `$ npm install jasmine` 81 | // Cucumber: `$ npm install cucumber` 82 | framework: 'mocha', 83 | // 84 | // Test reporter for stdout. 85 | // The following are supported: dot (default), spec and xunit 86 | // see also: http://webdriver.io/guide/testrunner/reporters.html 87 | reporter: 'spec', 88 | 89 | // 90 | // ===== 91 | // Hooks 92 | // ===== 93 | // Run functions before or after the test. If one of them return with a promise, WebdriverIO 94 | // will wait until that promise got resolved to continue. 95 | // see also: http://webdriver.io/guide/testrunner/hooks.html 96 | // 97 | // Gets executed before all workers get launched. 98 | onPrepare: function() { 99 | // do something 100 | }, 101 | // 102 | // Gets executed before test execution begins. At this point you will have access to all global 103 | // variables like `browser`. It is the perfect place to define custom commands. 104 | before: function() { 105 | // Set viewport width to 1200px so that no information is hidden due to responsive layouts 106 | browser.setViewportSize({width: 1200, height: 768}); 107 | }, 108 | // 109 | // Gets executed after all tests are done. You still have access to all global variables from 110 | // the test. 111 | after: function() { 112 | // do something 113 | }, 114 | // 115 | // Gets executed after all workers got shut down and the process is about to exit. It is not 116 | // possible to defer the end of the process using a promise. 117 | onComplete: function() { 118 | // do something 119 | } 120 | }; --------------------------------------------------------------------------------