├── .gitignore ├── jest.config.js ├── prettier.config.js ├── .mocharc.js ├── .github ├── workflows │ └── test.yml └── dependabot.yml ├── .eslintrc.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── tsconfig.json ├── src └── index.ts ├── README.md └── test └── tests.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .nyc_output/ 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | } 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | trailingComma: 'es5', 5 | } 6 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | require: 'ts-node/register', 3 | extensions: ['ts'], 4 | spec: ['src/*.test.ts'], 5 | 'watch-files': ['src'], 6 | }; 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test-data-bot tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [14.x, 16.x, 18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm ci 21 | - run: npm run build 22 | - run: npm run lint 23 | - run: npm run test-with-coverage 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "@types/faker" 10 | versions: 11 | - 5.1.5 12 | - 5.1.6 13 | - 5.1.7 14 | - 5.5.0 15 | - 5.5.1 16 | - dependency-name: y18n 17 | versions: 18 | - 4.0.1 19 | - 4.0.2 20 | - dependency-name: faker 21 | versions: 22 | - 5.2.0 23 | - 5.3.1 24 | - 5.4.0 25 | - 5.5.0 26 | - 5.5.1 27 | - 5.5.2 28 | - dependency-name: lodash 29 | versions: 30 | - 4.17.20 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | parser: '@typescript-eslint/parser', 7 | plugins: ['prettier', 'tap', '@typescript-eslint'], 8 | extends: ['prettier', 'plugin:@typescript-eslint/recommended'], 9 | rules: { 10 | 'prettier/prettier': ['error'], 11 | '@typescript-eslint/indent': 'off', 12 | '@typescript-eslint/explicit-function-return-type': 'off', 13 | '@typescript-eslint/no-empty-function': 'off', 14 | 15 | // Tap 16 | 'tap/no-identical-title': 'error', 17 | 'tap/no-ignored-test-files': 'error', 18 | 'tap/no-only-test': 'error', 19 | 'tap/no-skip-test': 'error', 20 | 'tap/no-statement-after-end': 'error', 21 | 'tap/test-ended': 'error', 22 | 'tap/use-t-well': 'error', 23 | 'tap/use-t': 'error', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jack Franklin 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jackfranklin/test-data-bot", 3 | "version": "2.1.0", 4 | "license": "MIT", 5 | "description": "Generate test data for your tests easily.", 6 | "engines": { 7 | "node": ">=12" 8 | }, 9 | "main": "build/index.js", 10 | "scripts": { 11 | "test": "tap --no-coverage -R=dot", 12 | "test-with-coverage": "tap -R=dot", 13 | "lint": "eslint '{src,test}/*.ts' && prettier --list-different '{src,test}/*.ts'", 14 | "lint-fix": "eslint --fix '{src,test}/*.ts' && prettier --write '{src,test}/*.ts'", 15 | "build": "tsc --build tsconfig.json", 16 | "prepare": "npm run build", 17 | "prepublishOnly": "npm run lint && npm run test" 18 | }, 19 | "tap": { 20 | "ts": true 21 | }, 22 | "keywords": [ 23 | "testing", 24 | "factory-bot", 25 | "fixtures", 26 | "test" 27 | ], 28 | "files": [ 29 | "build/" 30 | ], 31 | "author": "Jack Franklin", 32 | "homepage": "https://github.com/jackfranklin/test-data-bot#readme", 33 | "bugs": "https://github.com/jackfranklin/test-data-bot/issues", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/jackfranklin/test-data-bot.git" 37 | }, 38 | "devDependencies": { 39 | "@sinonjs/fake-timers": "^10.0.0", 40 | "@types/node": "^20.2.5", 41 | "@types/sinon": "^10.0.13", 42 | "@types/tap": "^15.0.7", 43 | "@typescript-eslint/eslint-plugin": "^5.6.0", 44 | "@typescript-eslint/parser": "^5.6.0", 45 | "eslint": "^8.4.1", 46 | "eslint-config-prettier": "^8.0.0", 47 | "eslint-config-unobtrusive": "^1.2.5", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "eslint-plugin-tap": "^1.2.1", 50 | "prettier": "^2.5.1", 51 | "sinon": "^15.0.1", 52 | "tap": "^16.3.0", 53 | "ts-node": "^10.9.1", 54 | "typescript": "^5.0.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.1.0 - 01 Mar 2023 2 | 3 | - New feature: Added `one(config)` as a helper method to create a single instance. This is equal to calling the created builder directly. [PR](https://github.com/jackfranklin/test-data-bot/pull/937) 4 | - New feature: Added `many(N, config)` as a helper method to create an array of `N` instances. [PR](https://github.com/jackfranklin/test-data-bot/pull/899). 5 | - Bug fix: incorrect overrriding of `Date` fields. [PR](https://github.com/jackfranklin/test-data-bot/pull/852) 6 | - Bug fix: allow traits to override undefined values. [PR](https://github.com/jackfranklin/test-data-bot/pull/866) 7 | 8 | ### 2.0.0 - 17 Mar 2022 9 | 10 | - **BREAKING** Removed Faker support. If you wish to use it, you may, and you can use the `perBuild` generator to integrate it. [PR](https://github.com/jackfranklin/test-data-bot/pull/603). 11 | - **BREAKING** Various TypeScript changes to improve type-safety of builders. May cause values that were previously `any` to be typed more strictly, and hence may need code changes [PR](https://github.com/jackfranklin/test-data-bot/pull/605). 12 | - **BREAKING** Dropped support for Node 10. Minimum Node version is now 12. 13 | - Dropped the dependency on Lodash [PR](https://github.com/jackfranklin/test-data-bot/pull/650). 14 | 15 | ### 1.4.0 - 09 Dec 2021 16 | 17 | _Sorry for the lack of updates and silence! I'm hopeful that I can maintain this library more proactively moving forwards and continue to improve test-data-bot._ 18 | 19 | - Updated Faker to latest version (v5.5.3) 20 | - Support non-truthy values in overrides [PR](https://github.com/jackfranklin/test-data-bot/pull/288) 21 | - Various dependency updates: latest TypeScript, Prettier, ESLint, Jest, and so on. 22 | 23 | ### 1.3.0 - 13 May 2020 24 | 25 | - Added traits to test-data-bot. See the README for examples. 26 | 27 | 28 | ### 1.2.0 - 09 May 2020 29 | 30 | - Factories now do not need a factory name property. You can simply pass in the configuration object : 31 | ```js 32 | const userBuilder = build({ ... }); 33 | // rather than: 34 | const userBuilder = build('User', { ... }); 35 | ``` 36 | 37 | You can still pass a name if you like, but it's not required and will probably be removed in a future major version. 38 | 39 | ### 1.1.0 - 23 March 2020 40 | 41 | - Fix: builders can now take literal `null` or `undefined` values: https://github.com/jackfranklin/test-data-bot/pull/198 42 | - Fix: `sequence` returns `unknown` not `number`: https://github.com/jackfranklin/test-data-bot/pull/196 43 | - Fix: ship Faker types for nice type hinting: https://github.com/jackfranklin/test-data-bot/pull/197 44 | - Upgrade to Prettier 2: https://github.com/jackfranklin/test-data-bot/pull/195 45 | - Swap from `yarn` to `npm`: https://github.com/jackfranklin/test-data-bot/pull/194 46 | 47 | 48 | ### 1.0.0 - 26 January 2020 49 | 50 | - Completely new version! See README for migration details. 51 | 52 | 53 | ### 0.8.0 - 04 March 2019 54 | 55 | - Add `numberBetween` - [PR by @spilist](https://github.com/jackfranklin/test-data-bot/pull/43). 56 | 57 | ### 0.7.1 - 20 Feb 2019 58 | 59 | - Fix passing primitive values like `null` or `undefined` to a builder. (https://github.com/jackfranklin/test-data-bot/issues/39) 60 | 61 | ### 0.7.0 - 24 Jan 2019 62 | 63 | - Fix a bug where you were unable to pass plain functions in as values to builders 64 | - Fix `arrayOf` behaviour so it can take a builder and correctly call it. 65 | 66 | ### 0.6.1- 01 Dec 2018 67 | 68 | - fix bug that meant overriding a boolean to always be `false` wouldn't happen (https://github.com/jackfranklin/test-data-bot/issues/6) 69 | 70 | ### 0.6.0- 03 August 2018 71 | 72 | - rebuilt to enable fully nested builders 73 | 74 | ### 0.5.0- 03 August 2018 75 | 76 | - add `arrayOf` 77 | - add `bool` 78 | - Enable `sequence` to take other builders. 79 | 80 | ### 0.4.0- 19 July 2018 81 | 82 | - add `oneOf` and `incrementingId` 83 | 84 | ### 0.3.0- 13 June 2018 85 | 86 | - Fix `main` in `package.json` not pointing in the right place- PR by [Kent C. Dodds](https://github.com/jackfranklin/test-data-bot/pull/1) 87 | 88 | ### 0.2.0- 12 June 2018 89 | 90 | - Added `perBuild` generator. 91 | 92 | ### 0.1.0- 11 June 2018 93 | 94 | - First release! 95 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "lib": [] /* Specify library files to be included in the compilation. */, 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 13 | "sourceMap": true /* Generates corresponding '.map' file. */, 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", 16 | "rootDir": "./src", 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "include": ["src/index.ts"], 64 | "exclude": ["src/*.test.ts"] 65 | } 66 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export interface SequenceGenerator { 2 | generatorType: 'sequence'; 3 | call: (counter: number) => T; 4 | } 5 | 6 | export interface PerBuildGenerator { 7 | generatorType: 'perBuild'; 8 | call: () => T; 9 | } 10 | 11 | export interface OneOfGenerator { 12 | generatorType: 'oneOf'; 13 | call: () => T; 14 | } 15 | 16 | export type FieldGenerator = 17 | | SequenceGenerator 18 | | OneOfGenerator 19 | | PerBuildGenerator; 20 | 21 | export type Field = T | FieldGenerator | FieldsConfiguration; 22 | 23 | export type FieldsConfiguration = { 24 | readonly [Key in keyof FactoryResultType]: Field; 25 | }; 26 | 27 | export type Overrides = { 28 | [Key in keyof FactoryResultType]?: Field; 29 | }; 30 | 31 | export interface BuildTimeConfig { 32 | overrides?: Overrides; 33 | map?: (builtThing: FactoryResultType) => FactoryResultType; 34 | traits?: string | string[]; 35 | } 36 | 37 | export interface TraitsConfiguration { 38 | readonly [traitName: string]: { 39 | overrides?: Overrides; 40 | postBuild?: (builtThing: FactoryResultType) => FactoryResultType; 41 | }; 42 | } 43 | 44 | export interface BuildConfiguration { 45 | readonly fields: FieldsConfiguration; 46 | readonly traits?: TraitsConfiguration; 47 | readonly postBuild?: (x: FactoryResultType) => FactoryResultType; 48 | } 49 | 50 | const isGenerator = (field: Field): field is FieldGenerator => { 51 | if (!field) return false; 52 | 53 | return (field as FieldGenerator).generatorType !== undefined; 54 | }; 55 | 56 | export type ValueOf = T[keyof T]; 57 | 58 | const identity = (x: T): T => x; 59 | 60 | const buildTimeTraitsArray = ( 61 | buildTimeConfig: BuildTimeConfig 62 | ): string[] => { 63 | const { traits = [] } = buildTimeConfig; 64 | return Array.isArray(traits) ? traits : [traits]; 65 | }; 66 | 67 | const getValueOrOverride = ( 68 | overrides: Overrides, 69 | traitOverrides: Overrides, 70 | fieldValue: Field, 71 | fieldKey: string 72 | ): Field => { 73 | if (Object.keys(overrides).includes(fieldKey)) { 74 | return overrides[fieldKey]; 75 | } 76 | 77 | if (Object.keys(traitOverrides).includes(fieldKey)) { 78 | return traitOverrides[fieldKey]; 79 | } 80 | 81 | return fieldValue; 82 | }; 83 | 84 | function mapValues( 85 | object: InputObject, 86 | callback: (value: InputObject[Key], key: Key) => unknown 87 | ) { 88 | return (Object.keys(object) as Key[]).reduce((total, key) => { 89 | total[key] = callback(object[key], key); 90 | return total; 91 | }, {} as { [key in Key]: unknown }); 92 | } 93 | 94 | export interface Builder { 95 | (buildTimeConfig?: BuildTimeConfig): FactoryResultType; 96 | reset(): void; 97 | one(buildTimeConfig?: BuildTimeConfig): FactoryResultType; 98 | many( 99 | count: number, 100 | buildTimeConfig?: BuildTimeConfig 101 | ): FactoryResultType[]; 102 | } 103 | 104 | export const build = ( 105 | factoryNameOrConfig: string | BuildConfiguration, 106 | configObject?: BuildConfiguration 107 | ): Builder => { 108 | const config = ( 109 | typeof factoryNameOrConfig === 'string' ? configObject : factoryNameOrConfig 110 | ) as BuildConfiguration; 111 | 112 | let sequenceCounter = 0; 113 | 114 | const expandConfigFields = ( 115 | fields: FieldsConfiguration, 116 | buildTimeConfig: BuildTimeConfig = {} 117 | ): { [P in keyof FieldsConfiguration]: any } => { 118 | const finalBuiltThing = mapValues(fields, (fieldValue, fieldKey) => { 119 | const overrides = buildTimeConfig.overrides || {}; 120 | 121 | const traitsArray = buildTimeTraitsArray(buildTimeConfig); 122 | 123 | const traitOverrides = traitsArray.reduce>( 124 | (overrides, currentTraitKey) => { 125 | const hasTrait = config.traits && config.traits[currentTraitKey]; 126 | if (!hasTrait) { 127 | console.warn(`Warning: trait '${currentTraitKey}' not found.`); 128 | } 129 | const traitsConfig = config.traits 130 | ? config.traits[currentTraitKey] 131 | : {}; 132 | return { ...overrides, ...(traitsConfig.overrides || {}) }; 133 | }, 134 | {} 135 | ); 136 | 137 | const valueOrOverride = getValueOrOverride( 138 | overrides, 139 | traitOverrides, 140 | fieldValue, 141 | fieldKey as string 142 | ); 143 | 144 | /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ 145 | return expandConfigField(valueOrOverride); 146 | }); 147 | 148 | return finalBuiltThing; 149 | }; 150 | 151 | const expandConfigField = (fieldValue: Field): Field => { 152 | if (isGenerator(fieldValue)) { 153 | switch (fieldValue.generatorType) { 154 | case 'sequence': { 155 | return fieldValue.call(++sequenceCounter); 156 | } 157 | 158 | case 'oneOf': 159 | case 'perBuild': { 160 | return fieldValue.call(); 161 | } 162 | } 163 | } 164 | 165 | if (Array.isArray(fieldValue)) { 166 | return fieldValue.map((v) => expandConfigField(v)); 167 | } 168 | 169 | if (fieldValue === null || fieldValue === undefined) { 170 | // has to be before typeof fieldValue === 'object' 171 | // as typeof null === 'object' 172 | return fieldValue; 173 | } 174 | 175 | if (fieldValue instanceof Date) { 176 | return fieldValue; 177 | } 178 | 179 | if (typeof fieldValue === 'object') { 180 | return expandConfigFields(fieldValue); 181 | } 182 | 183 | return fieldValue; 184 | }; 185 | 186 | const builder = ( 187 | buildTimeConfig: BuildTimeConfig = {} 188 | ) => { 189 | const fieldsToReturn = expandConfigFields(config.fields, buildTimeConfig); 190 | const traitsArray = buildTimeTraitsArray(buildTimeConfig); 191 | 192 | // A user might define a value in a trait that doesn't exist in the base 193 | // set of fields. So we need to check now if the traits set any values that 194 | // aren't in the base, and set them too. 195 | traitsArray.forEach((traitName) => { 196 | const traitConfig = (config.traits && config.traits[traitName]) || {}; 197 | if (!traitConfig.overrides) { 198 | return; 199 | } 200 | for (const stringKey of Object.keys(traitConfig.overrides)) { 201 | const key = stringKey as keyof FieldsConfiguration; 202 | // If the key already exists in the base fields, we'll have defined it, 203 | // so we don't need to worry about it. 204 | if (key in config.fields === false) { 205 | fieldsToReturn[key] = expandConfigField(traitConfig.overrides[key]); 206 | } 207 | } 208 | }); 209 | 210 | const traitPostBuilds = traitsArray.map((traitName) => { 211 | const traitConfig = (config.traits && config.traits[traitName]) || {}; 212 | const postBuild = traitConfig.postBuild || identity; 213 | return postBuild; 214 | }); 215 | 216 | const afterTraitPostBuildFields = traitPostBuilds.reduce( 217 | (fields, traitPostBuild) => { 218 | return traitPostBuild(fields); 219 | }, 220 | fieldsToReturn 221 | ); 222 | const postBuild = config.postBuild || identity; 223 | const buildTimeMapFunc = buildTimeConfig.map || identity; 224 | 225 | return buildTimeMapFunc(postBuild(afterTraitPostBuildFields)); 226 | }; 227 | 228 | builder.reset = () => { 229 | sequenceCounter = 0; 230 | }; 231 | 232 | builder.one = ( 233 | buildTimeConfig: BuildTimeConfig 234 | ): FactoryResultType => { 235 | return builder(buildTimeConfig); 236 | }; 237 | 238 | builder.many = ( 239 | times: number, 240 | buildTimeConfig: BuildTimeConfig 241 | ): FactoryResultType[] => { 242 | return new Array(times).fill(0).map(() => builder(buildTimeConfig)); 243 | }; 244 | 245 | return builder; 246 | }; 247 | 248 | export const oneOf = (...options: T[]): OneOfGenerator => { 249 | return { 250 | generatorType: 'oneOf', 251 | call: (): T => { 252 | const randomIndex = Math.floor(Math.random() * options.length); 253 | 254 | return options[randomIndex]; 255 | }, 256 | }; 257 | }; 258 | 259 | export const bool = () => oneOf(true, false); 260 | 261 | type Sequence = { 262 | (): SequenceGenerator; 263 | (userProvidedFunction: (count: number) => T): SequenceGenerator; 264 | }; 265 | 266 | export const sequence = ((userProvidedFunction) => { 267 | return { 268 | generatorType: 'sequence', 269 | call: (counter: number) => { 270 | if (typeof userProvidedFunction === 'undefined') { 271 | return counter; 272 | } 273 | return userProvidedFunction(counter); 274 | }, 275 | }; 276 | }) as Sequence; 277 | 278 | export const perBuild = (func: () => T): PerBuildGenerator => { 279 | return { 280 | generatorType: 'perBuild', 281 | call: () => { 282 | return func(); 283 | }, 284 | }; 285 | }; 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NO LONGER MAINTAINED 2 | 3 | **IMPORTANT: This project is no longer maintained and will be archived soon**. 4 | 5 | I recommend using [minicry-js](https://github.com/Stivooo/mimicry-js) which is a more modern, up to date factory library inspired by test-data-bot, and fixing some of the open issues and bugs. 6 | 7 | Alternatively, if you wish to help maintain test-data-bot, please feel free to open an issue or send PRs. 8 | 9 | # @jackfranklin/test-data-bot 10 | 11 | [![npm version](https://badge.fury.io/js/%40jackfranklin%2Ftest-data-bot.svg)](https://badge.fury.io/js/%40jackfranklin%2Ftest-data-bot) 12 | 13 | **IMPORTANT**: `@jackfranklin/test-data-bot` is the new version of this package, written in TypeScript and initially released as version 1.0.0. 14 | 15 | The old package, `test-data-bot` (_not scoped to my username on npm_) was last released as 0.8.0 and is not being updated any more. It is recommended to upgrade to 1.0.0, which has some breaking changes documented below. 16 | 17 | If you want to find the old documentation for `0.8.0`, you can [do so via an old README on GitHub](https://github.com/jackfranklin/test-data-bot/blob/c0fd856cbe8ea26024725aaca47e433fe727ddff/README.md). 18 | 19 | # Motivation and usage 20 | 21 | test-data-bot was inspired by [Factory Bot](https://github.com/thoughtbot/factory_bot), a Ruby library that makes it really easy to generate fake yet realistic looking data for your unit tests. 22 | 23 | Rather than creating random objects each time you want to test something in your system you can instead use a _factory_ that can create fake data. This keeps your tests consistent and means that they always use data that replicates the real thing. If your tests work off objects close to the real thing they are more useful and there's a higher chance of them finding bugs. 24 | 25 | _Rather than the term `factory`, test-data-bot uses `builder`._ 26 | 27 | test-data-bot makes no assumptions about frameworks or libraries, and can be used with any test runner too. test-data-bot is written in TypeScript, so if you use that you'll get nice type safety (see the TypeScript section of this README) but you can use it in JavaScript with no problems. 28 | 29 | ``` 30 | npm install --save-dev @jackfranklin/test-data-bot 31 | yarn add --dev @jackfranklin/test-data-bot 32 | ``` 33 | 34 | ## Creating your first builder 35 | 36 | We use the `build` function to create a builder. You give a builder an object of fields you want to define: 37 | 38 | ```js 39 | const { build } = require('@jackfranklin/test-data-bot'); 40 | 41 | const userBuilder = build({ 42 | fields: { 43 | name: 'jack', 44 | }, 45 | }); 46 | 47 | const user = userBuilder.one(); 48 | console.log(user); 49 | // => { name: 'jack'} 50 | ``` 51 | 52 | _While the examples in this README use `require`, you can also use `import {build} from '@jackfranklin/test-data-bot'`._ 53 | 54 | Once you've created a builder, you can call the `one` method it returns to generate an instance of that object - in this case, a `user`. 55 | 56 | ``` 57 | const user = userBuilder.one(); 58 | ``` 59 | 60 | > You can also call the builder directly to get a single instance - `userBuilder()`. v2.1 of test-data-bot shipped with the `one()` method and that is now the recommended way of constructing these objects. 61 | 62 | It would be boring though if each user had the same `name` - so test-data-bot lets you generate data via some API methods: 63 | 64 | ### Incrementing IDs with `sequence` 65 | 66 | Often you will be creating objects that have an ID that comes from a database, so you need to guarantee that it's unique. You can use `sequence`, which increments every time it's called: 67 | 68 | ```js 69 | const { build, sequence } = require('@jackfranklin/test-data-bot'); 70 | 71 | const userBuilder = build({ 72 | fields: { 73 | id: sequence(), 74 | }, 75 | }); 76 | 77 | const userOne = userBuilder.one(); 78 | const userTwo = userBuilder.one(); 79 | 80 | // userOne.id === 1 81 | // userTwo.id === 2 82 | ``` 83 | 84 | If you need more control, you can pass `sequence` a function that will be called with the number. This is useful to ensure completely unique emails, for example: 85 | 86 | ```js 87 | const { build, sequence } = require('@jackfranklin/test-data-bot'); 88 | 89 | const userBuilder = build({ 90 | fields: { 91 | email: sequence((x) => `jack${x}@gmail.com`), 92 | }, 93 | }); 94 | 95 | const userOne = userBuilder.one(); 96 | const userTwo = userBuilder.one(); 97 | 98 | // userOne.email === jack1@gmail.com 99 | // userTwo.email === jack2@gmail.com 100 | ``` 101 | 102 | You can use the `reset` method to reset the counter used internally when generating a sequence: 103 | 104 | ```js 105 | const { build, sequence } = require('@jackfranklin/test-data-bot'); 106 | 107 | const userBuilder = build({ 108 | fields: { 109 | id: sequence(), 110 | }, 111 | }); 112 | 113 | const userOne = userBuilder.one(); 114 | const userTwo = userBuilder.one(); 115 | userBuilder.reset(); 116 | const userThree = userBuilder.one(); 117 | const userFour = userBuilder.one(); 118 | 119 | // userOne.id === 1 120 | // userTwo.id === 2 121 | // userThree.id === 1 <- the sequence has been reset here 122 | // userFour.id === 2 123 | ``` 124 | 125 | ### Randomly picking between an option with `oneOf` 126 | 127 | If you want an object to have a random value, picked from a list you control, you can use `oneOf`: 128 | 129 | ```js 130 | const { build, oneOf } = require('@jackfranklin/test-data-bot'); 131 | 132 | const userBuilder = build({ 133 | fields: { 134 | name: oneOf('alice', 'bob', 'charlie'), 135 | }, 136 | }); 137 | ``` 138 | 139 | ### `bool` 140 | 141 | If you need something to be either `true` or `false`, you can use `bool`: 142 | 143 | ```js 144 | const { build, bool } = require('@jackfranklin/test-data-bot'); 145 | 146 | const userBuilder = build({ 147 | fields: { 148 | isAdmin: bool(), 149 | }, 150 | }); 151 | ``` 152 | 153 | ### `perBuild` 154 | 155 | test-data-bot lets you declare a field to always be a particular value: 156 | 157 | ```js 158 | const { build, perBuild } = require('@jackfranklin/test-data-bot'); 159 | 160 | const userBuilder = build({ 161 | fields: { 162 | name: 'jack', 163 | details: {}, 164 | }, 165 | }); 166 | ``` 167 | 168 | A user generated from this builder will always be the same data. However, if you generate two users using the builder above, they will have _exactly the same object_ for the `details` key: 169 | 170 | ```js 171 | const userOne = userBuilder.one(); 172 | const userTwo = userBuilder.one(); 173 | 174 | userOne.details === userTwo.details; // true 175 | ``` 176 | 177 | If you want to generate a unique object every time, you can use `perBuild` which takes a function and executes it when a builder is built: 178 | 179 | ```js 180 | const { build, perBuild } = require('@jackfranklin/test-data-bot'); 181 | 182 | const userBuilder = build({ 183 | fields: { 184 | name: 'jack', 185 | details: perBuild(() => { 186 | return {}; 187 | }), 188 | }, 189 | }); 190 | 191 | const userOne = userBuilder.one(); 192 | const userTwo = userBuilder.one(); 193 | 194 | userOne.details === userTwo.details; // false 195 | ``` 196 | 197 | This approach also lets you use any additional libraries, say if you wanted to use a library to generate fake data: 198 | 199 | ```js 200 | const myFakeLibrary = require('whatever-library-you-want'); 201 | const { build, perBuild } = require('@jackfranklin/test-data-bot'); 202 | 203 | const userBuilder = build({ 204 | fields: { 205 | name: perBuild(() => myFakeLibrary.randomName()), 206 | }, 207 | }); 208 | ``` 209 | 210 | ## Overrides per-build 211 | 212 | You'll often need to generate a random object but control one of the values directly for the purpose of testing. When you call a builder you can pass in overrides which will override the builder defaults: 213 | 214 | ```js 215 | const { build, fake, sequence } = require('@jackfranklin/test-data-bot'); 216 | 217 | const userBuilder = build({ 218 | fields: { 219 | id: sequence(), 220 | name: fake((f) => f.name.findName()), 221 | }, 222 | }); 223 | 224 | const user = userBuilder.one({ 225 | overrides: { 226 | id: 1, 227 | name: 'jack', 228 | }, 229 | }); 230 | 231 | // user.id === 1 232 | // user.name === 'jack' 233 | ``` 234 | 235 | If you need to edit the object directly, you can pass in a `map` function when you call the builder. This will be called after test-data-bot has generated the fake object, and lets you directly change its properties. 236 | 237 | ```js 238 | const { build, sequence } = require('@jackfranklin/test-data-bot'); 239 | 240 | const userBuilder = build('User', { 241 | fields: { 242 | id: sequence(), 243 | name: 'jack', 244 | }, 245 | }); 246 | 247 | const user = userBuilder.one({ 248 | map: (user) => { 249 | user.name = user.name.toUpperCase(); 250 | return user; 251 | }, 252 | }); 253 | ``` 254 | 255 | Using `overrides` and `map` lets you easily customise a specific object that a builder has created. 256 | 257 | ### Creating multiple instances 258 | 259 | If you want to create multiple instances of a builder at once, you can use the `many` method on the builder: 260 | 261 | ```js 262 | const userBuilder = build({ 263 | fields: { 264 | name: 'jack', 265 | }, 266 | }); 267 | 268 | const users = userBuilder.many(20); // Creates an array of 20 users. 269 | ``` 270 | 271 | If you want to pass in any build time configuration, you can pass in a second argument which takes the exact same configuration as calling `userBuilder()` directly: 272 | 273 | ```js 274 | const userBuilder = build({ 275 | fields: { 276 | name: 'jack', 277 | }, 278 | }); 279 | 280 | const users = userBuilder.many(20, { 281 | overrides: { 282 | name: 'bob', 283 | }, 284 | }); // Creates an array of 20 users, each called "bob"! 285 | ``` 286 | 287 | ### Mapping over all the created objects with `postBuild` 288 | 289 | If you need to transform an object in a way that test-data-bot doesn't support out the box, you can pass a `postBuild` function when creating a builder. This builder will run every time you create an object from it. 290 | 291 | ```js 292 | const { build, fake } = require('@jackfranklin/test-data-bot'); 293 | 294 | const userBuilder = build({ 295 | fields: { 296 | name: fake((f) => f.name.findName()), 297 | }, 298 | postBuild: (user) => { 299 | user.name = user.name.toUpperCase(); 300 | return user; 301 | }, 302 | }); 303 | 304 | const user = userBuilder.one(); 305 | // user.name will be uppercase 306 | ``` 307 | 308 | ## Traits (_new in v1.3_) 309 | 310 | Traits let you define a set of overrides for a factory that can easily be re-applied. Let's imagine you've got a users factory where users can be admins: 311 | 312 | ```ts 313 | interface User { 314 | name: string; 315 | admin: boolean; 316 | } 317 | 318 | const userBuilder = build({ 319 | fields: { 320 | name: 'jack', 321 | admin: false, 322 | }, 323 | traits: { 324 | admin: { 325 | overrides: { admin: true }, 326 | }, 327 | }, 328 | }); 329 | ``` 330 | 331 | Notice that we've defined the `admin` trait here. You don't need to do this; you could easily override the `admin` field each time: 332 | 333 | ```js 334 | const adminUser = userBuilder.one({ overrides: { admin: true } }); 335 | ``` 336 | 337 | But imagine that the field changes, or the way you represent admins changes. Or imagine setting an admin is not just one field but a few fields that need to change. Maybe an admin's email address always has to be a certain domain. We can define that behaviour once as a trait: 338 | 339 | ```ts 340 | const userBuilder = build({ 341 | fields: { 342 | name: 'jack', 343 | admin: false, 344 | }, 345 | traits: { 346 | admin: { 347 | overrides: { admin: true }, 348 | }, 349 | }, 350 | }); 351 | ``` 352 | 353 | And now building an admin user is easy: 354 | 355 | ```js 356 | const admin = userBuilder.one({ traits: 'admin' }); 357 | ``` 358 | 359 | You can define and use multiple traits when building an object. Be aware that if two traits override the same value, the one passed in last wins: 360 | 361 | ``` 362 | // any properties defined in other-trait will override any that admin sets 363 | const admin = userBuilder.one({ traits: ['admin', 'other-trait'] }); 364 | ``` 365 | 366 | ## TypeScript support 367 | 368 | test-data-bot is written in TypeScript and ships with the types generated so if you're using TypeScript you will get some nice type support out the box. 369 | 370 | The builders are generic, so you can describe to test-data-bot exactly what object you're creating: 371 | 372 | ```ts 373 | interface User { 374 | id: number; 375 | name: string; 376 | } 377 | 378 | const userBuilder = build('User', { 379 | fields: { 380 | id: sequence(), 381 | name: perBuild(() => yourCustomFakerLibary().name), 382 | }, 383 | }); 384 | 385 | const users = userBuilder.one(); 386 | ``` 387 | 388 | You should get TypeScript errors if the builder doesn't satisfy the interface you've given it. 389 | 390 | ## What happened to Faker / the `fake` generator? 391 | 392 | Prior to v2.0.0 of this library, we shipped built-in support for using Faker.js to generate data. It was removed because it was a big dependency to ship to all users, even those who don't use faker. If you want to use it you can, in combination with the `perBuild` builder: 393 | 394 | ```js 395 | import { build, perBuild } from '@jackfranklin/test-data-bot'; 396 | 397 | // This can be any fake data library you like. 398 | import fake from 'faker'; 399 | 400 | const userBuilder = build({ 401 | // Within perBuild, call your faker library directly. 402 | name: perBuild(() => fake().name()), 403 | }); 404 | ``` 405 | -------------------------------------------------------------------------------- /test/tests.ts: -------------------------------------------------------------------------------- 1 | import { build, sequence, oneOf, bool, perBuild } from '../src/index'; 2 | 3 | import sinon from 'sinon'; 4 | import tap from 'tap'; 5 | 6 | tap.afterEach(() => sinon.restore()); //eslint-disable-line tap/test-ended 7 | 8 | tap.test('can build a basic object from a factory', (t) => { 9 | interface User { 10 | name: string; 11 | } 12 | 13 | const userBuilder = build({ 14 | fields: { 15 | name: 'jack', 16 | }, 17 | }); 18 | 19 | const user = userBuilder(); 20 | t.same(user, { name: 'jack' }); 21 | t.end(); 22 | }); 23 | 24 | tap.test('you can use the .one() method to build a single instance', (t) => { 25 | interface User { 26 | name: string; 27 | } 28 | 29 | const userBuilder = build({ 30 | fields: { 31 | name: 'jack', 32 | }, 33 | }); 34 | 35 | const user = userBuilder.one(); 36 | t.same(user, { name: 'jack' }); 37 | t.end(); 38 | }); 39 | 40 | tap.test('the one() method supports overrides', (t) => { 41 | interface User { 42 | name: string; 43 | } 44 | 45 | const userBuilder = build({ 46 | fields: { 47 | name: 'jack', 48 | }, 49 | }); 50 | 51 | const user = userBuilder.one({ overrides: { name: 'alice' } }); 52 | t.same(user, { name: 'alice' }); 53 | t.end(); 54 | }); 55 | 56 | tap.test('can build an object with primitive values only', (t) => { 57 | interface User { 58 | name: string; 59 | } 60 | 61 | const userBuilder = build('User', { 62 | fields: { 63 | name: 'jack', 64 | }, 65 | }); 66 | 67 | const user = userBuilder(); 68 | t.same(user, { name: 'jack' }); 69 | t.end(); 70 | }); 71 | 72 | tap.test('lets you pass null in as a value', (t) => { 73 | interface User { 74 | name: string | null; 75 | } 76 | 77 | const userBuilder = build('User', { 78 | fields: { 79 | name: null, 80 | }, 81 | }); 82 | 83 | const user = userBuilder(); 84 | t.same(user, { name: null }); 85 | t.end(); 86 | }); 87 | 88 | tap.test('lets you pass undefined in as a value', (t) => { 89 | interface User { 90 | name?: string; 91 | } 92 | 93 | const userBuilder = build('User', { 94 | fields: { 95 | name: undefined, 96 | }, 97 | }); 98 | 99 | const user = userBuilder(); 100 | t.same(user, { name: undefined }); 101 | t.end(); 102 | }); 103 | 104 | tap.test('supports nulls in nested builders', (t) => { 105 | interface Address { 106 | street1: string; 107 | street2: string | null; 108 | city: string; 109 | state: string; 110 | zipCode: string; 111 | } 112 | interface Company { 113 | id: string; 114 | name: string; 115 | mailingAddress: Address; 116 | } 117 | 118 | const addressBuilder = build
('Address', { 119 | fields: { 120 | street1: perBuild(() => 'some street'), 121 | street2: null, 122 | city: 'city', 123 | state: 'state', 124 | zipCode: 'zip', 125 | }, 126 | }); 127 | 128 | const companyBuilder = build('Company', { 129 | fields: { 130 | id: '123', 131 | name: 'Test', 132 | mailingAddress: perBuild(addressBuilder), 133 | }, 134 | }); 135 | 136 | const company = companyBuilder(); 137 | tap.equal(company.mailingAddress.street2, null); 138 | t.end(); 139 | }); 140 | 141 | tap.test('lets a value be overriden when building an instance', (t) => { 142 | interface User { 143 | name: string; 144 | } 145 | 146 | const userBuilder = build('User', { 147 | fields: { 148 | name: perBuild(() => 'jack'), 149 | }, 150 | }); 151 | 152 | const user = userBuilder({ overrides: { name: 'customName' } }); 153 | t.same(user, { 154 | name: 'customName', 155 | }); 156 | t.end(); 157 | }); 158 | 159 | tap.test('lets a value be overridden with 0 when building an instance', (t) => { 160 | interface Product { 161 | amount: number; 162 | } 163 | 164 | const productBuilder = build('Product', { 165 | fields: { 166 | amount: 10, 167 | }, 168 | }); 169 | 170 | const product = productBuilder({ overrides: { amount: 0 } }); 171 | t.same(product, { 172 | amount: 0, 173 | }); 174 | t.end(); 175 | }); 176 | 177 | tap.test( 178 | 'lets a value be overridden with null when building an instance', 179 | (t) => { 180 | interface User { 181 | name: string | null; 182 | } 183 | 184 | const userBuilder = build('User', { 185 | fields: { 186 | name: 'name', 187 | }, 188 | }); 189 | 190 | const user = userBuilder({ overrides: { name: null } }); 191 | t.same(user, { 192 | name: null, 193 | }); 194 | t.end(); 195 | } 196 | ); 197 | 198 | tap.test('perBuild generates a new object each time', (t) => { 199 | interface User { 200 | data: Record; 201 | } 202 | 203 | const userBuilder = build('User', { 204 | fields: { 205 | data: perBuild(() => ({})), 206 | }, 207 | }); 208 | 209 | const user1 = userBuilder(); 210 | const user2 = userBuilder(); 211 | 212 | t.same(user1.data, {}); 213 | t.same(user2.data, {}); 214 | t.not(user1.data, user2.data); 215 | t.end(); 216 | }); 217 | tap.test('sequence gets incremented per build', (t) => { 218 | interface User { 219 | id: number; 220 | } 221 | 222 | const userBuilder = build('User', { 223 | fields: { 224 | id: sequence(), 225 | }, 226 | }); 227 | 228 | const users = [userBuilder(), userBuilder()]; 229 | t.same(users, [{ id: 1 }, { id: 2 }]); 230 | t.end(); 231 | }); 232 | 233 | tap.test('sequence can take a function that returns a string', (t) => { 234 | interface User { 235 | id: string; 236 | } 237 | 238 | const userBuilder = build('User', { 239 | fields: { 240 | id: sequence((x) => `jack${x}@gmail.com`), 241 | }, 242 | }); 243 | 244 | const user = userBuilder(); 245 | t.same(user, { id: 'jack1@gmail.com' }); 246 | t.end(); 247 | }); 248 | 249 | tap.test('can take a function to return a number', (t) => { 250 | interface User { 251 | id: number; 252 | } 253 | 254 | const userBuilder = build('User', { 255 | fields: { 256 | id: sequence((x) => x * 10), 257 | }, 258 | }); 259 | 260 | const users = [userBuilder(), userBuilder()]; 261 | t.same(users, [{ id: 10 }, { id: 20 }]); 262 | t.end(); 263 | }); 264 | tap.test('can have the sequence be manually reset', (t) => { 265 | interface User { 266 | id: number; 267 | } 268 | 269 | const userBuilder = build('User', { 270 | fields: { 271 | id: sequence((x) => x ** 2), 272 | }, 273 | }); 274 | 275 | const usersGroup1 = [userBuilder(), userBuilder(), userBuilder()]; 276 | t.same(usersGroup1, [{ id: 1 }, { id: 4 }, { id: 9 }]); 277 | 278 | userBuilder.reset(); 279 | 280 | const usersGroup2 = [userBuilder(), userBuilder(), userBuilder()]; 281 | t.same(usersGroup2, [{ id: 1 }, { id: 4 }, { id: 9 }]); 282 | t.end(); 283 | }); 284 | 285 | tap.test('can have a simple sequence be manually reset', (t) => { 286 | interface User { 287 | id: number; 288 | } 289 | 290 | const userBuilder = build('User', { 291 | fields: { 292 | id: sequence(), 293 | }, 294 | }); 295 | 296 | const usersGroup1 = [userBuilder(), userBuilder(), userBuilder()]; 297 | t.same(usersGroup1, [{ id: 1 }, { id: 2 }, { id: 3 }]); 298 | 299 | userBuilder.reset(); 300 | 301 | const usersGroup2 = [userBuilder(), userBuilder(), userBuilder()]; 302 | t.same(usersGroup2, [{ id: 1 }, { id: 2 }, { id: 3 }]); 303 | t.end(); 304 | }); 305 | 306 | tap.test( 307 | 'lets you map over the generated object to fully customise it', 308 | (t) => { 309 | interface User { 310 | name: string; 311 | sports: { 312 | football: boolean; 313 | rugby: boolean; 314 | }; 315 | } 316 | 317 | const userBuilder = build('User', { 318 | fields: { 319 | name: perBuild(() => 'jack'), 320 | sports: { 321 | football: true, 322 | rugby: false, 323 | }, 324 | }, 325 | }); 326 | 327 | const user = userBuilder({ 328 | overrides: { 329 | name: 'customName', 330 | }, 331 | map: (user) => { 332 | user.sports.rugby = true; 333 | return user; 334 | }, 335 | }); 336 | 337 | t.equal(user.name, 'customName'); 338 | t.same(user.sports, { 339 | football: true, 340 | rugby: true, 341 | }); 342 | t.end(); 343 | } 344 | ); 345 | 346 | tap.test('lets you define the map on the builder level as postBuild', (t) => { 347 | interface User { 348 | name: string; 349 | } 350 | 351 | const userBuilder = build('User', { 352 | postBuild: (user) => { 353 | user.name = user.name.toUpperCase(); 354 | return user; 355 | }, 356 | fields: { 357 | name: perBuild(() => 'jack'), 358 | }, 359 | }); 360 | 361 | const user = userBuilder(); 362 | t.equal(user.name, 'JACK'); 363 | t.end(); 364 | }); 365 | 366 | tap.test('runs the postBuild function after applying overrides', (t) => { 367 | interface User { 368 | name: string; 369 | } 370 | 371 | const userBuilder = build('User', { 372 | postBuild: (user) => { 373 | user.name = user.name.toUpperCase(); 374 | return user; 375 | }, 376 | fields: { 377 | name: perBuild(() => 'test'), 378 | }, 379 | }); 380 | 381 | const user = userBuilder({ 382 | overrides: { 383 | name: 'jack', 384 | }, 385 | }); 386 | t.equal(user.name, 'JACK'); 387 | t.end(); 388 | }); 389 | 390 | tap.test('the build time map function runs after postBuild', (t) => { 391 | t.plan(2); 392 | interface User { 393 | name: string; 394 | } 395 | 396 | const userBuilder = build('User', { 397 | postBuild: (user) => { 398 | user.name = user.name.toUpperCase(); 399 | return user; 400 | }, 401 | fields: { 402 | name: 'test', 403 | }, 404 | }); 405 | 406 | const user = userBuilder({ 407 | overrides: { 408 | name: 'jack', 409 | }, 410 | map: (user) => { 411 | t.equal(user.name, 'JACK'); 412 | user.name = 'new name'; 413 | return user; 414 | }, 415 | }); 416 | t.equal(user.name, 'new name'); 417 | t.end(); 418 | }); 419 | 420 | tap.test('bool is provided as a shortcut for oneOf(true, false)', (t) => { 421 | interface User { 422 | admin: boolean; 423 | } 424 | 425 | const userBuilder = build('User', { 426 | fields: { 427 | admin: bool(), 428 | }, 429 | }); 430 | 431 | const user = userBuilder(); 432 | t.type(user.admin, 'boolean'); 433 | t.end(); 434 | }); 435 | 436 | tap.test('picks a random entry from the given selection', (t) => { 437 | interface User { 438 | name: string; 439 | } 440 | 441 | const userBuilder = build('User', { 442 | fields: { 443 | name: oneOf('a', 'b', 'c'), 444 | }, 445 | }); 446 | 447 | const user = userBuilder(); 448 | t.ok(['a', 'b', 'c'].includes(user.name)); 449 | t.end(); 450 | }); 451 | 452 | tap.test('nested arrays get fully expanded', (t) => { 453 | interface User { 454 | friends: { 455 | names: string[]; 456 | }; 457 | } 458 | 459 | const userBuilder = build('User', { 460 | fields: { 461 | friends: { 462 | names: [perBuild(() => 'test1'), 'test2'], 463 | }, 464 | }, 465 | }); 466 | 467 | const user = userBuilder(); 468 | t.same(user.friends.names, ['test1', 'test2']); 469 | t.end(); 470 | }); 471 | 472 | tap.test('fully expands super nested awkward things', (t) => { 473 | interface Friend { 474 | name: string; 475 | sports: { 476 | [x: string]: boolean; 477 | }; 478 | } 479 | 480 | interface User { 481 | name: string; 482 | friends: Friend[]; 483 | } 484 | 485 | const friendBuilder = build('Friend', { 486 | fields: { 487 | name: perBuild(() => 'some name'), 488 | sports: { 489 | football: bool(), 490 | basketball: false, 491 | rugby: true, 492 | }, 493 | }, 494 | }); 495 | 496 | const userBuilder = build('User', { 497 | fields: { 498 | name: 'jack', 499 | friends: [ 500 | friendBuilder({ overrides: { name: 'customName' } }), 501 | friendBuilder({ 502 | overrides: { 503 | sports: { 504 | rugby: false, 505 | }, 506 | }, 507 | }), 508 | ], 509 | }, 510 | }); 511 | 512 | const user = userBuilder(); 513 | t.equal(user.name, 'jack'); 514 | t.same(user.friends, [ 515 | { 516 | name: 'customName', 517 | sports: { 518 | football: user.friends[0].sports.football, 519 | basketball: false, 520 | rugby: true, 521 | }, 522 | }, 523 | { 524 | name: user.friends[1].name, 525 | sports: { 526 | rugby: false, 527 | }, 528 | }, 529 | ]); 530 | t.end(); 531 | }); 532 | 533 | tap.test('fully expands objects to ensure all builders are executed', (t) => { 534 | interface User { 535 | details: { 536 | name: string; 537 | }; 538 | admin: boolean; 539 | } 540 | 541 | const userBuilder = build('User', { 542 | fields: { 543 | details: { 544 | name: perBuild(() => 'test name'), 545 | }, 546 | admin: bool(), 547 | }, 548 | }); 549 | 550 | const user = userBuilder(); 551 | 552 | t.type(user.details.name, 'string'); 553 | t.type(user.admin, 'boolean'); 554 | t.end(); 555 | }); 556 | 557 | tap.test('does not call postBuild on nested objects', (t) => { 558 | interface User { 559 | name: string; 560 | sports: { 561 | football: boolean; 562 | basketball: boolean; 563 | rugby: boolean; 564 | }; 565 | } 566 | 567 | const userBuilder = build('User', { 568 | postBuild: (user) => ({ 569 | ...user, 570 | name: 'new name', 571 | }), 572 | fields: { 573 | name: 'old name', 574 | sports: { 575 | football: true, 576 | basketball: false, 577 | rugby: true, 578 | }, 579 | }, 580 | }); 581 | 582 | const user = userBuilder(); 583 | 584 | t.same(user, { 585 | name: 'new name', 586 | sports: { 587 | football: true, 588 | basketball: false, 589 | rugby: true, 590 | }, 591 | }); 592 | t.end(); 593 | }); 594 | 595 | tap.test('allows a trait to be defined and then used', (t) => { 596 | interface User { 597 | name: string; 598 | admin: boolean; 599 | } 600 | 601 | const userBuilder = build({ 602 | fields: { 603 | name: 'jack', 604 | admin: perBuild(() => false), 605 | }, 606 | traits: { 607 | admin: { 608 | overrides: { admin: perBuild(() => true) }, 609 | }, 610 | }, 611 | }); 612 | 613 | const userNoTrait = userBuilder(); 614 | const userWithTrait = userBuilder({ traits: 'admin' }); 615 | t.equal(userNoTrait.admin, false); 616 | t.equal(userWithTrait.admin, true); 617 | t.end(); 618 | }); 619 | 620 | tap.test('allows a trait to define a postBuild function', (t) => { 621 | interface User { 622 | name: string; 623 | admin: boolean; 624 | } 625 | 626 | const userBuilder = build({ 627 | fields: { 628 | name: 'jack', 629 | admin: perBuild(() => false), 630 | }, 631 | traits: { 632 | admin: { 633 | overrides: { admin: perBuild(() => true) }, 634 | postBuild: (user) => { 635 | user.name = 'postBuildTrait'; 636 | return user; 637 | }, 638 | }, 639 | }, 640 | }); 641 | 642 | const userNoTrait = userBuilder(); 643 | const userWithTrait = userBuilder({ traits: 'admin' }); 644 | t.equal(userNoTrait.name, 'jack'); 645 | t.equal(userWithTrait.name, 'postBuildTrait'); 646 | t.end(); 647 | }); 648 | 649 | tap.test('applies build time overrides over traits', (t) => { 650 | interface User { 651 | name: string; 652 | admin: boolean; 653 | } 654 | 655 | const userBuilder = build({ 656 | fields: { 657 | name: 'jack', 658 | admin: perBuild(() => false), 659 | }, 660 | traits: { 661 | admin: { 662 | overrides: { admin: perBuild(() => true) }, 663 | }, 664 | }, 665 | }); 666 | 667 | const userWithTrait = userBuilder({ 668 | traits: 'admin', 669 | overrides: { 670 | admin: perBuild(() => false), 671 | }, 672 | }); 673 | t.notOk(userWithTrait.admin); 674 | t.end(); 675 | }); 676 | 677 | tap.test('supports multiple traits', (t) => { 678 | interface User { 679 | name: string; 680 | admin: boolean; 681 | } 682 | 683 | const userBuilder = build({ 684 | fields: { 685 | name: 'jack', 686 | admin: perBuild(() => false), 687 | }, 688 | traits: { 689 | admin: { 690 | overrides: { admin: perBuild(() => true) }, 691 | }, 692 | bob: { 693 | overrides: { name: 'bob' }, 694 | }, 695 | }, 696 | }); 697 | 698 | const userWithTrait = userBuilder({ 699 | traits: ['admin', 'bob'], 700 | }); 701 | t.same(userWithTrait, { 702 | name: 'bob', 703 | admin: true, 704 | }); 705 | t.end(); 706 | }); 707 | 708 | tap.test('traits passed later override earlier ones', (t) => { 709 | interface User { 710 | name: string; 711 | } 712 | 713 | const userBuilder = build({ 714 | fields: { 715 | name: 'jack', 716 | }, 717 | traits: { 718 | alice: { 719 | overrides: { name: 'alice' }, 720 | }, 721 | bob: { 722 | overrides: { name: 'bob' }, 723 | }, 724 | }, 725 | }); 726 | 727 | const userWithTrait = userBuilder({ 728 | traits: ['alice', 'bob'], 729 | }); 730 | t.same(userWithTrait, { 731 | name: 'bob', 732 | }); 733 | t.end(); 734 | }); 735 | 736 | tap.test('logs a warning if you pass a trait that was not defined', (t) => { 737 | interface User { 738 | name: string; 739 | } 740 | 741 | const consoleStub = sinon.stub(console, 'warn').callsFake(() => {}); 742 | 743 | const userBuilder = build({ 744 | fields: { 745 | name: 'jack', 746 | }, 747 | }); 748 | const userWithTrait = userBuilder({ 749 | traits: 'not-passed', 750 | }); 751 | 752 | t.same(userWithTrait, { name: 'jack' }); 753 | t.ok( 754 | consoleStub.calledOnceWithExactly("Warning: trait 'not-passed' not found.") 755 | ); 756 | t.end(); 757 | }); 758 | 759 | tap.test('traits can override undefined values', (t) => { 760 | interface User { 761 | name: string; 762 | tall?: boolean; 763 | } 764 | 765 | const userBuilder = build({ 766 | fields: { 767 | name: 'jack', 768 | }, 769 | traits: { 770 | tall: { 771 | overrides: { tall: true }, 772 | }, 773 | }, 774 | }); 775 | 776 | const userWithoutTrait = userBuilder(); 777 | t.equal(userWithoutTrait.tall, undefined); 778 | const userWithTrait = userBuilder({ 779 | traits: ['tall'], 780 | }); 781 | t.same(userWithTrait, { 782 | name: 'jack', 783 | tall: true, 784 | }); 785 | t.end(); 786 | }); 787 | 788 | tap.test('the latest trait with the value defined "wins"', (t) => { 789 | interface User { 790 | name: string; 791 | tall?: boolean; 792 | } 793 | 794 | const userBuilder = build({ 795 | fields: { 796 | name: 'jack', 797 | }, 798 | traits: { 799 | tall: { 800 | overrides: { tall: true }, 801 | }, 802 | short: { 803 | overrides: { tall: false }, 804 | }, 805 | }, 806 | }); 807 | const userWithTrait = userBuilder({ 808 | traits: ['tall', 'short'], 809 | }); 810 | t.same(userWithTrait, { 811 | name: 'jack', 812 | tall: false, 813 | }); 814 | t.end(); 815 | }); 816 | 817 | tap.test('dates can be created and overwritten correctly', (t) => { 818 | interface Plan { 819 | createdAt: Date; 820 | } 821 | 822 | const planBuilder = build('Plan', { 823 | fields: { 824 | createdAt: perBuild(() => new Date()), 825 | }, 826 | }); 827 | 828 | const plan = planBuilder(); 829 | t.type(plan.createdAt, 'Date'); 830 | 831 | const planWithCustomDate = planBuilder({ 832 | overrides: { 833 | createdAt: new Date(), 834 | }, 835 | }); 836 | t.type(planWithCustomDate.createdAt, 'Date'); 837 | 838 | t.end(); 839 | }); 840 | 841 | tap.test('can create multiple instances of a builder easily', (t) => { 842 | interface User { 843 | name: string; 844 | } 845 | const userBuilder = build({ 846 | fields: { 847 | name: 'jack', 848 | }, 849 | }); 850 | const users = userBuilder.many(20); 851 | t.same(users.length, 20); 852 | for (const user of users) { 853 | t.same(user, { name: 'jack' }); 854 | } 855 | t.end(); 856 | }); 857 | 858 | tap.test('can create multiple instances and override them', (t) => { 859 | interface User { 860 | name: string; 861 | } 862 | const userBuilder = build({ 863 | fields: { 864 | name: 'jack', 865 | }, 866 | }); 867 | const users = userBuilder.many(20, { 868 | overrides: { 869 | name: 'bob', 870 | }, 871 | }); 872 | t.same(users.length, 20); 873 | for (const user of users) { 874 | t.same(user, { name: 'bob' }); 875 | } 876 | t.end(); 877 | }); 878 | 879 | tap.test('can create multiple instances with traits', (t) => { 880 | interface User { 881 | name: string; 882 | } 883 | const userBuilder = build({ 884 | fields: { 885 | name: 'jack', 886 | }, 887 | traits: { 888 | calledBob: { 889 | overrides: { 890 | name: 'bob', 891 | }, 892 | }, 893 | }, 894 | }); 895 | const users = userBuilder.many(20, { 896 | traits: ['calledBob'], 897 | }); 898 | t.same(users.length, 20); 899 | for (const user of users) { 900 | t.same(user, { name: 'bob' }); 901 | } 902 | t.end(); 903 | }); 904 | 905 | tap.test('each instance of build.many is unique', (t) => { 906 | interface User { 907 | name: string; 908 | } 909 | const userBuilder = build({ 910 | fields: { 911 | name: 'jack', 912 | }, 913 | }); 914 | const users = userBuilder.many(20); 915 | t.same(users.length, 20); 916 | // Ensure that all users are unique objects 917 | t.same(new Set(users).size, 20); 918 | t.end(); 919 | }); 920 | --------------------------------------------------------------------------------