├── .all-contributorsrc ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── license ├── lint-staged.config.js ├── package.json ├── readme.md ├── src ├── __snapshots__ │ └── index.test.ts.snap ├── index.test.ts └── index.ts ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "reducer-tester", 3 | "projectOwner": "akameco", 4 | "files": [ 5 | "readme.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": false, 9 | "contributors": [ 10 | { 11 | "login": "akameco", 12 | "name": "akameco", 13 | "avatar_url": "https://avatars2.githubusercontent.com/u/4002137?v=4", 14 | "profile": "http://akameco.github.io", 15 | "contributions": [ 16 | "code", 17 | "doc", 18 | "test", 19 | "infra" 20 | ] 21 | }, 22 | { 23 | "login": "adhrinae", 24 | "name": "Ahn Dohyung", 25 | "avatar_url": "https://avatars2.githubusercontent.com/u/14539203?v=4", 26 | "profile": "https://adhrinae.github.io", 27 | "contributions": [ 28 | "doc" 29 | ] 30 | }, 31 | { 32 | "login": "kinakobo", 33 | "name": "kinakobo", 34 | "avatar_url": "https://avatars3.githubusercontent.com/u/17736005?v=4", 35 | "profile": "https://github.com/kinakobo", 36 | "contributions": [ 37 | "doc" 38 | ] 39 | } 40 | ], 41 | "repoType": "github" 42 | } 43 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "zero" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/coverage/** 2 | **/flow-typed/** 3 | **/dest/** 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["precure/auto"], 3 | "rules": { 4 | "jest/consistent-test-it": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.lock binary 4 | package-lock.json binary 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | - version: 10 | - `node` version: 11 | - `npm` (or `yarn`) version: 12 | 13 | **Do you want to request a *feature* or report a *bug*?:** 14 | 15 | **What is the current behavior?:** 16 | 17 | **What is the expected behavior?:** 18 | 19 | **Suggested solution:** 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | **What**: 8 | 9 | 10 | 11 | **Why**: 12 | 13 | 14 | 15 | **How**: 16 | 17 | 18 | **Checklist**: 19 | 20 | 21 | * [ ] Documentation 22 | * [ ] Tests 23 | * [ ] Ready to be merged 24 | * [ ] Added myself to contributors table 25 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dest 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | flow-typed 2 | lib 3 | dest 4 | coverage 5 | .github 6 | package.json 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | trailingComma: es5 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | 5 | cache: 6 | yarn: true 7 | directories: 8 | - ".eslintcache" 9 | - "node_modules" 10 | 11 | script: 12 | - yarn run build 13 | - yarn run test:ci 14 | 15 | notifications: 16 | email: false 17 | 18 | branches: 19 | only: master 20 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) akameco (akameco.github.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.+(js|jsx|ts|tsx)': ['eslint --fix', 'jest --findRelatedTests'], 3 | '*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx)': [ 4 | 'prettier --write', 5 | 'git add', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reducer-tester", 3 | "version": "1.1.3", 4 | "description": "reducer tester", 5 | "license": "MIT", 6 | "repository": "akameco/reducer-tester", 7 | "author": "akameco (https://akameco.github.io)", 8 | "engines": { 9 | "node": ">=6" 10 | }, 11 | "scripts": { 12 | "add-contributor": "all-contributors add", 13 | "fmt": "prettier --write '**/*.{js,json,md}'", 14 | "build": "tsc", 15 | "prepare": "yarn build", 16 | "lint": "eslint src/**/*.ts", 17 | "test": "jest", 18 | "test:cov": "jest --coverage --ci --runInBand", 19 | "test:ci": "yarn lint && yarn test:cov" 20 | }, 21 | "files": [ 22 | "dest" 23 | ], 24 | "main": "dest", 25 | "types": "dest/index.d.ts", 26 | "keywords": [ 27 | "test", 28 | "jest", 29 | "redux", 30 | "react", 31 | "snapshot", 32 | "diff", 33 | "reducer", 34 | "test" 35 | ], 36 | "dependencies": { 37 | "invariant": "^2.2.4", 38 | "snapshot-diff": "^0.5.1" 39 | }, 40 | "devDependencies": { 41 | "@akameco/tsconfig": "0.3.0", 42 | "@types/invariant": "2.2.30", 43 | "@types/jest": "24.0.18", 44 | "@types/jest-diff": "20.0.1", 45 | "@types/redux": "3.6.0", 46 | "all-contributors-cli": "6.8.1", 47 | "eslint": "6.2.2", 48 | "eslint-config-precure": "5.0.2", 49 | "husky": "3.0.4", 50 | "jest": "24.9.0", 51 | "lint-staged": "9.2.3", 52 | "prettier": "1.18.2", 53 | "redux": "4.0.4", 54 | "ts-jest": "24.0.2", 55 | "typescript": "3.5.3" 56 | }, 57 | "husky": { 58 | "hooks": { 59 | "pre-commit": "lint-staged" 60 | } 61 | }, 62 | "jest": { 63 | "preset": "ts-jest", 64 | "modulePathIgnorePatterns": [ 65 | "dest" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # reducer-tester 2 | 3 | [![Build Status](https://travis-ci.org/akameco/reducer-tester.svg?branch=master)](https://travis-ci.org/akameco/reducer-tester) 4 | [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 5 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors) 7 | 8 | > reducer tester 9 | 10 | See [how-to-test-reducers](https://github.com/akameco/how-to-test-reducers) 11 | 12 | ## Install 13 | 14 | ``` 15 | $ yarn add --dev reducer-tester 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | // reducer.js 22 | export const initialState = { count: 0, other: 'other' } 23 | 24 | export default (state = initialState, action) => { 25 | switch (action.type) { 26 | case 'inc': 27 | return { ...state, count: state.count + 1 } 28 | case 'dec': 29 | return { ...state, count: state.count - 1 } 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | // reducer.test.js 36 | import reducerTester from 'reducer-tester' 37 | import reducer, { initialState } from './reducer' 38 | 39 | reducerTester({ 40 | reducer, 41 | state: initialState, 42 | tests: [{ type: 'inc' }, { type: 'dec' }], 43 | }) 44 | ``` 45 | 46 | ### Snapshot 47 | 48 | ```diff 49 | // Jest Snapshot v1, https://goo.gl/fbAQLP 50 | 51 | exports[`handle initial state 1`] = ` 52 | "Snapshot Diff: 53 | Compared values have no visual difference." 54 | `; 55 | 56 | exports[`dec 1`] = ` 57 | "Snapshot Diff: 58 | - Before 59 | + After 60 | 61 | Object { 62 | - "count": 0, 63 | + "count": -1, 64 | "other": "other", 65 | }" 66 | `; 67 | 68 | exports[`inc 1`] = ` 69 | "Snapshot Diff: 70 | - Before 71 | + After 72 | 73 | Object { 74 | - "count": 0, 75 | + "count": 1, 76 | "other": "other", 77 | } 78 | `; 79 | ``` 80 | 81 | ## Full example 82 | 83 | ```js 84 | import reducerTester from 'reducer-tester' 85 | import reducer, { initialState } from './reducer' 86 | 87 | reducerTester({ 88 | reducer, // #required 89 | state: initialState, // #required 90 | tests: [{ type: 'inc' }, { type: 'dec' }], // #required 91 | initialTest: false, // # optional, default: true 92 | titlePrefix: 'handle ', // # optional, default: '' 93 | }) 94 | ``` 95 | 96 | ```diff 97 | // Jest Snapshot v1, https://goo.gl/fbAQLP 98 | 99 | exports[`handle dec 1`] = ` 100 | "Snapshot Diff: 101 | - Before 102 | + After 103 | 104 | Object { 105 | - "count: 0, 106 | + count: -1, 107 | } 108 | `; 109 | 110 | exports[`handle inc 1`] = ` 111 | "Snapshot Diff: 112 | - Before 113 | + After 114 | 115 | Object { 116 | - count: 0, 117 | + count: 1, 118 | } 119 | `; 120 | ``` 121 | 122 | ### Tips 123 | 124 | #### Atom Editor User 125 | 126 | Install `language-diff` and `file-types`. And open `config.json` and edit as blow. 127 | 128 | ```cson 129 | "*": 130 | "file-types": 131 | "\\.js\\.snap$": "source.diff" 132 | ``` 133 | 134 | Hooray! Very readable! 135 | 136 | ![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f31353331392f64666537363137312d323735322d646265302d613038652d6330633436646330396264662e706e67 (495×575)](https://camo.qiitausercontent.com/d621872e2fedd535ccdb694170499d2ee7031080/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f31353331392f64666537363137312d323735322d646265302d613038652d6330633436646330396264662e706e67) 137 | 138 | ## Contributors 139 | 140 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 141 | 142 | 143 | 144 |
akameco
akameco

💻 📖 ⚠️ 🚇
Ahn Dohyung
Ahn Dohyung

📖
kinakobo
kinakobo

📖
145 | 146 | 147 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 148 | 149 | ## License 150 | 151 | MIT © [akameco](http://akameco.github.io) 152 | -------------------------------------------------------------------------------- /src/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`handle initial state 1`] = ` 4 | Snapshot Diff: 5 | Compared values have no visual difference. 6 | `; 7 | 8 | exports[`handle initial state 2`] = ` 9 | Snapshot Diff: 10 | Compared values have no visual difference. 11 | `; 12 | 13 | exports[`handle work 1`] = ` 14 | Snapshot Diff: 15 | - Before 16 | + After 17 | 18 | - Object {} 19 | + Object { 20 | + "result": "payload", 21 | + } 22 | `; 23 | 24 | exports[`throws an invariant if state property is not exist 1`] = `"required \`state\` property."`; 25 | 26 | exports[`throws an invariant if tests is not Array 1`] = `"required \`tests\` property."`; 27 | 28 | exports[`throws an invariant if type property is not exist 1`] = `"{\\"typo\\":\\"typo\\"} Action required \`type\` property."`; 29 | 30 | exports[`work 1`] = ` 31 | Snapshot Diff: 32 | - Before 33 | + After 34 | 35 | - Object {} 36 | + Object { 37 | + "result": "payload", 38 | + } 39 | `; 40 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux' 2 | import reducerTester from '.' 3 | 4 | const noop = (): void => {} 5 | const reducer = (state: object = {}, action: AnyAction): object => { 6 | if (action.payload) { 7 | return { result: action.payload } 8 | } 9 | return state 10 | } 11 | 12 | let itSpy: jest.SpyInstance 13 | 14 | const state = {} 15 | 16 | beforeEach(() => { 17 | // @ts-ignore 18 | itSpy = jest.spyOn(global, 'it').mockImplementation(noop) 19 | }) 20 | 21 | afterEach(() => { 22 | itSpy.mockRestore() 23 | }) 24 | 25 | test('throws an invariant if tests is not Array', () => { 26 | expect(() => { 27 | // @ts-ignore 28 | reducerTester({ reducer, state, tests: null }) 29 | }).toThrowErrorMatchingSnapshot() 30 | }) 31 | 32 | test('throws an invariant if type property is not exist', () => { 33 | expect(() => { 34 | // @ts-ignore 35 | reducerTester({ reducer, state, tests: [{ typo: 'typo' }] }) 36 | }).toThrowErrorMatchingSnapshot() 37 | }) 38 | 39 | test('throws an invariant if state property is not exist', () => { 40 | expect(() => { 41 | // @ts-ignore 42 | reducerTester({ reducer, tests: [] }) 43 | }).toThrowErrorMatchingSnapshot() 44 | }) 45 | 46 | test('can provide an object for tests', () => { 47 | const title = 'reducer-test' 48 | reducerTester({ 49 | reducer, 50 | state, 51 | tests: [{ type: title, payload: 'payload' }], 52 | }) 53 | expect(itSpy).toHaveBeenCalledTimes(2) 54 | expect(itSpy).toHaveBeenCalledWith(title, expect.any(Function)) 55 | expect(itSpy).toHaveBeenCalledWith( 56 | 'handle initial state', 57 | expect.any(Function) 58 | ) 59 | }) 60 | 61 | test('can provide empty array for tests', () => { 62 | reducerTester({ tests: [], reducer, state }) 63 | expect(itSpy).toHaveBeenCalledTimes(1) 64 | }) 65 | 66 | test('can provide initialTest', () => { 67 | reducerTester({ 68 | reducer, 69 | state, 70 | tests: [{ type: 'test' }], 71 | initialTest: true, 72 | }) 73 | 74 | expect(itSpy).toHaveBeenCalledTimes(2) 75 | expect(itSpy).toHaveBeenCalledWith( 76 | 'handle initial state', 77 | expect.any(Function) 78 | ) 79 | }) 80 | 81 | test('not handle initial state with initialTest = false', () => { 82 | reducerTester({ 83 | reducer, 84 | state, 85 | tests: [{ type: 'test' }], 86 | initialTest: false, 87 | }) 88 | 89 | expect(itSpy).toHaveBeenCalledTimes(1) 90 | }) 91 | 92 | test('can provide titlePrefix', () => { 93 | const title = 'reducer' 94 | reducerTester({ 95 | reducer, 96 | state, 97 | titlePrefix: 'handle ', 98 | tests: [{ type: title, payload: 'payload' }], 99 | }) 100 | expect(itSpy).toHaveBeenCalledTimes(2) 101 | expect(itSpy).toHaveBeenCalledWith('handle reducer', expect.any(Function)) 102 | }) 103 | 104 | // haha... jest work :) 105 | reducerTester({ 106 | reducer, 107 | state, 108 | tests: [{ type: 'work', payload: 'payload' }], 109 | }) 110 | 111 | reducerTester({ 112 | reducer, 113 | state, 114 | titlePrefix: 'handle ', 115 | tests: [{ type: 'work', payload: 'payload' }], 116 | }) 117 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-explicit-any: 0 */ 2 | import snapshotDiff from 'snapshot-diff' 3 | import invariant from 'invariant' 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import { Action as ReduxAction, AnyAction, Reducer } from 'redux' 6 | 7 | expect.addSnapshotSerializer(snapshotDiff.getSnapshotDiffSerializer()) 8 | 9 | /** 10 | * Definition of options for `reducerTester` function 11 | * 12 | * @param TesterOption - Available options for testing reducer. 13 | * @property tests - Array of Actions. 14 | * @property reducer - The reducer function going to be tested. 15 | * @property state - Redux State, should be the same type as the argument of reducer function which provided before. 16 | * @property {optional} initialTest - Tells reducerTester if this test is testing initial state. 17 | * @property {optional} titlePrefix - Set prefix for each action snapshots. otherwise snapshot title will be just action type. 18 | */ 19 | type TesterOption = { 20 | tests: Action[] 21 | reducer: Reducer 22 | state: State 23 | initialTest?: boolean 24 | titlePrefix?: string 25 | } 26 | 27 | export default function reducerTester({ 28 | tests, 29 | reducer, 30 | state, 31 | initialTest = true, 32 | titlePrefix = '', 33 | }: TesterOption): void { 34 | if (initialTest) { 35 | it('handle initial state', () => { 36 | expect( 37 | snapshotDiff(state, reducer(undefined, { type: '@@INIT' })) 38 | ).toMatchSnapshot() 39 | }) 40 | } 41 | 42 | invariant(state, 'required `state` property.') 43 | invariant(tests, 'required `tests` property.') 44 | invariant(Array.isArray(tests), 'tests must be a Array.') 45 | 46 | for (const t of tests) { 47 | invariant(t.type, `${JSON.stringify(t)} Action required \`type\` property.`) 48 | 49 | it(`${titlePrefix}${t.type}`, () => { 50 | expect( 51 | snapshotDiff(state, reducer(state, t), { 52 | expand: true, 53 | aAnnotation: 'Before', 54 | bAnnotation: 'After', 55 | }) 56 | ).toMatchSnapshot() 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@akameco/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["dom", "es2015"], 5 | "target": "es5", 6 | "outDir": "dest" 7 | }, 8 | "include": ["src"], 9 | "exclude": ["dest", "node_modules"] 10 | } 11 | --------------------------------------------------------------------------------