├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .nvmrc ├── .nycrc ├── LICENSE ├── README.md ├── config ├── development.env └── test.env ├── migrations └── 20180723222235-createUsers.js ├── package-lock.json ├── package.json ├── scripts └── db │ ├── create.js │ ├── createMigration.js │ ├── drop.js │ ├── migrate.js │ └── rollback.js ├── src ├── initialize.js └── models │ ├── index.js │ └── user │ └── index.js └── test ├── factories ├── index.js └── user.js ├── mocha.opts ├── setup.js ├── truncate.js └── unit └── models └── user.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["module-resolver", { 4 | "root": ["./"] 5 | }], 6 | "transform-es2015-modules-commonjs", 7 | "syntax-object-rest-spread" 8 | ], 9 | "env": { 10 | "test": { 11 | "plugins": [ 12 | "istanbul" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore all build related directories 2 | dist/ 3 | build/ 4 | coverage/ 5 | .nyc_output/ 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "extends": "eslint-config-riipen", 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 8 10 | }, 11 | "rules": { 12 | "import/no-extraneous-dependencies": 0, 13 | "import/imports-first": "off", 14 | "import/no-named-as-default-member": "off" 15 | }, 16 | "settings": { 17 | "import/resolver": { 18 | "babel-module": {} 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage directories (istanbul) 12 | coverage 13 | .nyc_output 14 | 15 | # Dependency directories 16 | node_modules 17 | 18 | # Optional npm cache directory 19 | .npm 20 | 21 | # Optional REPL history 22 | .node_repl_history 23 | 24 | # Builds 25 | dist/ 26 | build/ 27 | 28 | # OSX 29 | *.DS_Store 30 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.13.0 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "per-file": true, 4 | "lines": 0, 5 | "statements": 0, 6 | "functions": 0, 7 | "branches": 0, 8 | "include": [ 9 | "src/**/*.js" 10 | ], 11 | "require": [ 12 | "babel-register" 13 | ], 14 | "reporter": [ 15 | "lcov", 16 | "text-summary" 17 | ], 18 | "cache": true, 19 | "sourceMap": false, 20 | "instrument": false 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Riipen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testing-with-sequelize 2 | 3 | An example repository for how to test with the Sequelize ORM. 4 | 5 | ## Development 6 | 7 | A quick guide to get a new development environment setup 8 | 9 | ### Setup 10 | 11 | **Node** 12 | 13 | 1. Install [nvm][] with: 14 | ```bash 15 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.7/install.sh | bash`. 16 | 2. Install Node `8.11.2` with: 17 | ```bash 18 | nvm install 8.11.2 19 | ``` 20 | 3. From the root directory of this project, run 21 | ```bash 22 | nvm use 8.11.2 23 | ``` 24 | 4. Install NPM packages: 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | [nvm]: https://github.com/nvm-sh/nvm 30 | 31 | **Database** 32 | 33 | 1. Install PostgreSQL on your local machine. Use Homebrew as below or go to the [Postgres Downloads](https://www.postgresql.org/download/) page. 34 | 35 | ```bash 36 | $ brew install postgresql 37 | ``` 38 | 39 | 2. Start PostgreSQL and run on startup. 40 | 41 | ```bash 42 | $ brew services start postgresql 43 | ``` 44 | 45 | 3. Ensure a `root` user exists on PostgreSQL with no password: 46 | 47 | ```console 48 | $ psql --dbname=postgres 49 | postgres=# CREATE USER root; 50 | postgres=# ALTER USER root WITH SUPERUSER; 51 | ``` 52 | 53 | 4. Create the database by running: 54 | 55 | ```bash 56 | $ NODE_ENV=test npm run db:create 57 | ``` 58 | 59 | ### Testing 60 | 61 | Run the test suite: 62 | 63 | ```bash 64 | $ npm run test 65 | ``` 66 | -------------------------------------------------------------------------------- /config/development.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | POSTGRES_SERVICE_URL=postgres://root@localhost:5432/development?logging=false 3 | -------------------------------------------------------------------------------- /config/test.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | POSTGRES_SERVICE_URL=postgres://root@localhost:5432/test?logging=false 3 | -------------------------------------------------------------------------------- /migrations/20180723222235-createUsers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | queryInterface.createTable('users', { 4 | id: { 5 | type: Sequelize.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | email: { 10 | type: Sequelize.STRING, 11 | allowNull: false, 12 | }, 13 | first_name: { 14 | type: Sequelize.STRING, 15 | }, 16 | last_name: { 17 | type: Sequelize.STRING, 18 | }, 19 | created_at: { 20 | type: Sequelize.DATE, 21 | }, 22 | updated_at: { 23 | type: Sequelize.DATE, 24 | }, 25 | }), 26 | 27 | down: (queryInterface) => 28 | queryInterface.dropTable('users'), 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing-with-sequelize", 3 | "version": "0.0.3", 4 | "description": "An example repository for ", 5 | "repository": "https://github.com/jordanell/testing-with-sequelize", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Jordan Ell", 9 | "email": "me@jordanell.com" 10 | }, 11 | "engines": { 12 | "node": ">=10.13.0" 13 | }, 14 | "scripts": { 15 | "db:clean": "npm run db:drop && npm run db:create && npm run db:migrate", 16 | "db:create": "babel-node ./scripts/db/create", 17 | "db:create-migration": "babel-node ./scripts/db/createMigration", 18 | "db:drop": "babel-node ./scripts/db/drop", 19 | "db:migrate": "babel-node ./scripts/db/migrate", 20 | "db:rollback": "babel-node ./scripts/db/rollback", 21 | "lint": "eslint .", 22 | "lint:fix": "eslint . --fix", 23 | "pretest": "NODE_ENV=test npm run db:migrate", 24 | "test": "NODE_ENV=test mocha", 25 | "test:coverage": "NODE_ENV=test nyc mocha", 26 | "test:report": "nyc report --reporter=text-summary" 27 | }, 28 | "dependencies": { 29 | "auto-parse": "1.5.1", 30 | "babel-core": "6.26.3", 31 | "child-process-promise": "2.2.1", 32 | "dotenv": "6.1.0", 33 | "lodash": "4.17.21", 34 | "pg": "7.6.1", 35 | "pg-hstore": "2.3.2", 36 | "query-string": "6.2.0", 37 | "sequelize": "5.21.13", 38 | "sequelize-cli": "5.5.1", 39 | "whatwg-url": "7.0.0" 40 | }, 41 | "devDependencies": { 42 | "babel-cli": "6.26.0", 43 | "babel-eslint": "10.0.1", 44 | "babel-plugin-istanbul": "5.1.0", 45 | "babel-plugin-module-resolver": "3.1.1", 46 | "babel-plugin-root-import": "6.1.0", 47 | "babel-plugin-syntax-object-rest-spread": "6.13.0", 48 | "babel-plugin-transform-class-properties": "6.24.1", 49 | "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", 50 | "babel-register": "6.26.0", 51 | "braces": "2.3.1", 52 | "chai": "4.2.0", 53 | "eslint": "5.9.0", 54 | "eslint-config-riipen": "2.0.1", 55 | "eslint-import-resolver-babel-module": "4.0.0", 56 | "eslint-plugin-import": "2.14.0", 57 | "eslint-plugin-jsx-a11y": "6.1.2", 58 | "eslint-plugin-react": "7.11.1", 59 | "faker": "4.1.0", 60 | "kind-of": "6.0.3", 61 | "mocha": "8.2.1", 62 | "nyc": "15.1.0", 63 | "require-directory": "2.1.1", 64 | "sinon": "7.1.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scripts/db/create.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { exec } from 'child-process-promise'; 4 | import { parseURL } from 'whatwg-url'; 5 | 6 | import 'src/initialize'; 7 | 8 | const spawnOptions = { cwd: path.join(__dirname, '../..'), stdio: 'inherit' }; 9 | 10 | (async () => { 11 | const parts = parseURL(process.env.POSTGRES_SERVICE_URL); 12 | 13 | try { 14 | console.log('Create running'); 15 | await exec( 16 | `createdb -U root -h ${parts.host} -p ${parts.port} -O root ${parts.path[0]}`, 17 | spawnOptions 18 | ); 19 | console.log('*************************'); 20 | console.log('Create successful'); 21 | } catch (err) { 22 | console.log('*************************'); 23 | console.log('Create failed. Error:', err.message); 24 | } 25 | 26 | process.exit(0); 27 | })(); 28 | -------------------------------------------------------------------------------- /scripts/db/createMigration.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { spawn } from 'child-process-promise'; 4 | import { parseURL } from 'whatwg-url'; 5 | 6 | import 'src/initialize'; 7 | 8 | const spawnOptions = { cwd: path.join(__dirname, '../..'), stdio: 'inherit' }; 9 | 10 | (async () => { 11 | const parts = parseURL(process.env.POSTGRES_SERVICE_URL); 12 | 13 | // Strip our search params 14 | const url = `${parts.scheme}://${parts.username}@${parts.host}:${parts.port || 5432}/${parts.path[0]}`; 15 | 16 | try { 17 | await spawn('./node_modules/.bin/sequelize', ['migration:create', '--name', process.argv[2], `--url=${url}`], spawnOptions); 18 | console.log('*************************'); 19 | console.log('Migration creation successful'); 20 | } catch (err) { 21 | console.log('*************************'); 22 | console.log('Migration creation failed. Error:', err.message); 23 | } 24 | 25 | process.exit(0); 26 | })(); 27 | -------------------------------------------------------------------------------- /scripts/db/drop.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { exec } from 'child-process-promise'; 4 | import { parseURL } from 'whatwg-url'; 5 | 6 | import 'src/initialize'; 7 | 8 | const spawnOptions = { cwd: path.join(__dirname, '../..'), stdio: 'inherit' }; 9 | 10 | (async () => { 11 | const parts = parseURL(process.env.POSTGRES_SERVICE_URL); 12 | 13 | try { 14 | console.log('Drop running'); 15 | await exec(`dropdb -U root ${parts.path[0]}`, spawnOptions); 16 | console.log('*************************'); 17 | console.log('Drop successful'); 18 | } catch (err) { 19 | console.log('*************************'); 20 | console.log('Drop failed. Error:', err.message); 21 | } 22 | 23 | process.exit(0); 24 | })(); 25 | -------------------------------------------------------------------------------- /scripts/db/migrate.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { spawn } from 'child-process-promise'; 4 | import { parseURL } from 'whatwg-url'; 5 | 6 | import 'src/initialize'; 7 | 8 | const spawnOptions = { cwd: path.join(__dirname, '../..'), stdio: 'inherit' }; 9 | 10 | (async () => { 11 | const parts = parseURL(process.env.POSTGRES_SERVICE_URL); 12 | 13 | // Strip our search params 14 | const url = `${parts.scheme}://${parts.username}:${parts.password}@${parts.host}:${parts.port || 5432}/${parts.path[0]}`; 15 | 16 | try { 17 | await spawn('./node_modules/.bin/sequelize', ['db:migrate', `--url=${url}`], spawnOptions); 18 | console.log('*************************'); 19 | console.log('Migration successful'); 20 | } catch (err) { 21 | console.log('*************************'); 22 | console.log('Migration failed. Error:', err.message); 23 | } 24 | 25 | process.exit(0); 26 | })(); 27 | -------------------------------------------------------------------------------- /scripts/db/rollback.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { spawn } from 'child-process-promise'; 4 | import { parseURL } from 'whatwg-url'; 5 | 6 | import 'src/initialize'; 7 | 8 | const spawnOptions = { cwd: path.join(__dirname, '../..'), stdio: 'inherit' }; 9 | 10 | (async () => { 11 | const parts = parseURL(process.env.POSTGRES_SERVICE_URL); 12 | 13 | // Strip our search params 14 | const url = `${parts.scheme}://${parts.username}@${parts.host}:${parts.port || 5432}/${parts.path[0]}`; 15 | 16 | try { 17 | await spawn('./node_modules/.bin/sequelize', ['db:migrate:undo', `--url=${url}`], spawnOptions); 18 | console.log('*************************'); 19 | console.log('Migration successful'); 20 | } catch (err) { 21 | console.log('*************************'); 22 | console.log('Migration failed. Error:', err.message); 23 | } 24 | 25 | process.exit(0); 26 | })(); 27 | -------------------------------------------------------------------------------- /src/initialize.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import dotenv from 'dotenv'; 5 | import defaults from 'lodash/defaults'; 6 | 7 | // Default environment is development to prevent "accidents" 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 9 | 10 | // Reads in the needed config file from config/ 11 | const env = dotenv.parse(fs.readFileSync(path.resolve( 12 | __dirname, 13 | '..', 14 | 'config', 15 | `${process.env.NODE_ENV}.env` 16 | ))); 17 | 18 | // Sets all values from the config file 19 | defaults(process.env, env); 20 | -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file simple imports all Sequelize models and configures 3 | * their associations. 4 | * 5 | * Import this file to gain access to all Sequelize models 6 | * in other files. 7 | */ 8 | 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | 12 | import autoParse from 'auto-parse'; 13 | import { 14 | includes, 15 | omit, 16 | assign, 17 | } from 'lodash'; 18 | import queryString from 'query-string'; 19 | import Sequelize from 'sequelize'; 20 | import { parseURL } from 'whatwg-url'; 21 | 22 | // Convert postgres "bigint" columns to integers 23 | require('pg').defaults.parseInt8 = true; 24 | 25 | const urlParts = parseURL(process.env.POSTGRES_SERVICE_URL); 26 | const query = queryString.parse(urlParts.query || ''); urlParts.query = null; 27 | const pathname = urlParts.path[0]; urlParts.path = []; 28 | const username = urlParts.username; 29 | const password = urlParts.password; 30 | 31 | const options = omit(urlParts, 'query', 'path', 'username', 'password'); 32 | assign(options, query); 33 | 34 | ['logging'].forEach(x => { if (options[x]) options[x] = autoParse(options[x]); }); 35 | 36 | options.dialect = 'postgres'; 37 | options.pool = { 38 | max: 5, 39 | min: 0, 40 | acquire: 30000, 41 | idle: 10000, 42 | }; 43 | options.dialectOptions = {}; 44 | options.operatorsAliases = Sequelize.Op; 45 | const sequelize = new Sequelize(pathname, username, password, options); 46 | const db = {}; 47 | 48 | const IGNORE_FILES = [ 49 | '.DS_Store', 50 | 'index.js', 51 | ]; 52 | 53 | fs 54 | .readdirSync(__dirname) 55 | .filter((file) => !includes(IGNORE_FILES, file)) 56 | .forEach((file) => { 57 | const model = sequelize.import(path.join(__dirname, file)); 58 | 59 | db[model.name] = model; 60 | }); 61 | 62 | Object.keys(db).forEach((modelName) => { 63 | if ('associate' in db[modelName]) { 64 | db[modelName].associate(db); 65 | } 66 | }); 67 | 68 | db.sequelize = sequelize; 69 | db.Sequelize = Sequelize; 70 | 71 | export default db; 72 | -------------------------------------------------------------------------------- /src/models/user/index.js: -------------------------------------------------------------------------------- 1 | export default (sequelize, DataTypes) => 2 | sequelize.define('User', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true, 7 | }, 8 | email: { 9 | type: DataTypes.STRING, 10 | unique: { 11 | name: 'users_email', 12 | msg: 'A user with this email already exists.', 13 | }, 14 | allowNull: false, 15 | validate: { 16 | notEmpty: true, 17 | isEmail: true, 18 | }, 19 | }, 20 | first_name: { 21 | type: DataTypes.STRING, 22 | validate: { 23 | len: { 24 | args: [0, 255], 25 | }, 26 | }, 27 | }, 28 | last_name: { 29 | type: DataTypes.STRING, 30 | validate: { 31 | len: { 32 | args: [0, 255], 33 | }, 34 | }, 35 | }, 36 | }, { 37 | tableName: 'users', 38 | underscored: true, 39 | }); 40 | -------------------------------------------------------------------------------- /test/factories/index.js: -------------------------------------------------------------------------------- 1 | import { forEach } from 'lodash'; 2 | import requireDirectory from 'require-directory'; 3 | 4 | const factories = requireDirectory(module, './'); 5 | 6 | forEach(factories, (value, key) => { factories[key] = value.default; }); 7 | 8 | export default factories; 9 | -------------------------------------------------------------------------------- /test/factories/user.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | import models from 'src/models'; 4 | 5 | /** 6 | * Generate an object which contains attributes needed 7 | * to successfully create a user instance. 8 | * 9 | * @param {Object} props Properties to use for the user. 10 | * 11 | * @return {Object} An object to build the user from. 12 | */ 13 | const data = async (props = {}) => { 14 | const defaultProps = { 15 | email: faker.internet.email(), 16 | first_name: faker.name.firstName(), 17 | last_name: faker.name.lastName(), 18 | }; 19 | 20 | return Object.assign({}, defaultProps, props); 21 | }; 22 | 23 | /** 24 | * Generates a user instance from the properties provided. 25 | * 26 | * @param {Object} props Properties to use for the user. 27 | * 28 | * @return {Object} A user instance 29 | */ 30 | export default async (props = {}) => 31 | models.User.create(await data(props)); 32 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --exit 2 | --recursive 3 | --require babel-register 4 | --require test/setup.js 5 | --slow 2 6 | --timeout 10000 7 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import 'src/initialize'; 2 | -------------------------------------------------------------------------------- /test/truncate.js: -------------------------------------------------------------------------------- 1 | import models from 'src/models'; 2 | 3 | const truncateTable = (modelName) => 4 | models[modelName].destroy({ 5 | where: {}, 6 | force: true, 7 | }); 8 | 9 | export default async function truncate(model) { 10 | if (model) { 11 | return truncateTable(model); 12 | } 13 | 14 | return Promise.all( 15 | Object.keys(models).map((key) => { 16 | if (['sequelize', 'Sequelize'].includes(key)) return null; 17 | return truncateTable(key); 18 | }) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /test/unit/models/user.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | 3 | import models from 'src/models'; 4 | 5 | import factories from 'test/factories'; 6 | import truncate from 'test/truncate'; 7 | 8 | describe('User model', () => { 9 | let user; 10 | 11 | beforeEach(async () => { 12 | await truncate(); 13 | 14 | user = await factories.user(); 15 | }); 16 | 17 | it('should generate a user from the factory', async () => { 18 | assert.isOk(user.id); 19 | }); 20 | 21 | it('should truncate the user table with each test', async () => { 22 | const count = await models.User.count(); 23 | 24 | assert.equal(count, 1); 25 | }); 26 | }); 27 | --------------------------------------------------------------------------------