├── jest ├── __mocks__ │ ├── styleMock.js │ └── fileMock.js ├── setup.js ├── fileTransformer.js └── generator │ ├── createJestTest.js │ ├── index.js │ └── generateFile.js ├── .storybook └── addons.js ├── src ├── models.ts ├── utils │ ├── sum.ts │ └── sum.spec.ts ├── components │ ├── NotFound.tsx │ └── __tests__ │ │ ├── __snapshots__ │ │ └── NotFound.spec.tsx.snap │ │ └── NotFound.spec.tsx ├── containers │ └── Home.tsx ├── Root.tsx ├── routes.tsx ├── store │ ├── index.ts │ ├── configureStore.prod.ts │ └── configureStore.dev.ts ├── index.html ├── client.tsx ├── favicon.svg └── server.ts ├── .hound.yml ├── codecov.yml ├── commitlint.config.js ├── assets ├── image │ └── testing-structure.png └── ppt │ └── testing-structure.pptx ├── tsconfig.prod.json ├── dist ├── public │ ├── client.7f1954cbc82189d2016b.js.gz │ ├── favicon.svg │ ├── index.html │ └── client.7f1954cbc82189d2016b.js └── server.js ├── .prettierrc ├── .gitignore ├── CHANGELOG.md ├── tslint.json ├── tsconfig.json ├── .lintstagedrc ├── .vscode └── launch.json ├── .stylelintrc ├── jest.config.js ├── .circleci └── config.yml ├── config ├── server.config.js ├── client.config.js └── helpers.js ├── LICENSE ├── package.json └── README.md /jest/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /jest/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Todo: 测试环境初始化配置 3 | */ 4 | 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-jest/register'; 2 | -------------------------------------------------------------------------------- /jest/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | const models = {}; 2 | 3 | export default models; 4 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | tslint: 2 | enabled: true 3 | config_file: tslint.json 4 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: 293b95d6-2edc-4a1f-8ddd-52b26e95a65e -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /src/utils/sum.ts: -------------------------------------------------------------------------------- 1 | function sum(a, b) { 2 | return a + b; 3 | } 4 | 5 | export default sum; 6 | -------------------------------------------------------------------------------- /assets/image/testing-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingGe/defensor-automated-testing/HEAD/assets/image/testing-structure.png -------------------------------------------------------------------------------- /assets/ppt/testing-structure.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingGe/defensor-automated-testing/HEAD/assets/ppt/testing-structure.pptx -------------------------------------------------------------------------------- /src/components/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const NotFound = () => 404 Not Found; 4 | 5 | export default NotFound; 6 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "build" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /dist/public/client.7f1954cbc82189d2016b.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingGe/defensor-automated-testing/HEAD/dist/public/client.7f1954cbc82189d2016b.js.gz -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/NotFound.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NotFound Should render NotFound correctly 1`] = ` 4 | 5 | 404 Not Found 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /jest/fileTransformer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | process(src, filename, config, options) { 5 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/containers/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class Home extends React.Component { 4 | public componentDidCatch(ex: Error) { 5 | console.log(ex.message); 6 | } 7 | public render() { 8 | return Hello world; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | coverage 3 | reports 4 | tmp 5 | built 6 | build 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | .nyc_output/ 11 | .awcache 12 | .DS_Store 13 | .env 14 | .chrome/ 15 | .generate_config.json 16 | package-lock.json 17 | *.orig 18 | jest-test-results.json -------------------------------------------------------------------------------- /src/Root.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { hot } from 'react-hot-loader'; 3 | import { ConnectedRouter } from 'react-router-redux'; 4 | 5 | import routes from './routes'; 6 | 7 | const Root = ({ history }) => { 8 | return {routes}; 9 | }; 10 | 11 | export default hot(module)(Root); 12 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import NotFound from './components/NotFound'; 5 | import Home from './containers/Home'; 6 | 7 | const routes = ( 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default routes; 15 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-any 2 | let configureStore: any; 3 | 4 | // tslint:disable-next-line:prefer-conditional-expression 5 | if (process.env.NODE_ENV === 'production') { 6 | // tslint:disable-next-line:no-var-requires 7 | configureStore = require('./configureStore.prod'); 8 | } else { 9 | // tslint:disable-next-line:no-var-requires 10 | configureStore = require('./configureStore.dev'); 11 | } 12 | 13 | export default configureStore; 14 | -------------------------------------------------------------------------------- /src/components/__tests__/NotFound.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as Enzyme from 'enzyme'; 2 | import * as Adapter from 'enzyme-adapter-react-16'; 3 | import toJson from 'enzyme-to-json'; 4 | import * as React from 'react'; 5 | import NotFound from '../NotFound'; 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | 9 | describe('NotFound', () => { 10 | test('Should render NotFound correctly', () => { 11 | const wrapper = Enzyme.shallow(); 12 | expect(toJson(wrapper)).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.1.3](https://github.com/TingGe/defensor-automated-testing/compare/v0.1.2...v0.1.3) (2018-09-03) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * update README.md ([a043b1b](https://github.com/TingGe/defensor-automated-testing/commit/a043b1b)) 8 | 9 | 10 | ### Features 11 | 12 | * add command test:createTests and vscode debug ([54046c8](https://github.com/TingGe/defensor-automated-testing/commit/54046c8)) 13 | 14 | 15 | 16 | 17 | ## 0.1.2 (2018-08-30) 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | A React, Redux, TypeScript and Webpack starter 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/client.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import Root from './Root'; 7 | import configureStore from './store'; 8 | 9 | const store = configureStore.default(); 10 | const history = configureStore.history; 11 | 12 | render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root'), 19 | ); 20 | -------------------------------------------------------------------------------- /src/utils/sum.spec.ts: -------------------------------------------------------------------------------- 1 | import sum from './sum'; 2 | 3 | describe('sum equal ', () => { 4 | beforeAll(() => { 5 | console.log('sum before all'); 6 | }); 7 | 8 | afterAll(() => { 9 | console.log('sum after all'); 10 | }); 11 | 12 | beforeEach(() => { 13 | console.log('sum before each'); 14 | }); 15 | 16 | afterEach(() => { 17 | console.log('sum after each'); 18 | }); 19 | 20 | it('test 1 + 2 = 3 ', () => { 21 | expect(sum(1, 2)).toBe(3); 22 | }); 23 | 24 | it('test 2 + 3 = 5', () => { 25 | expect(sum(2, 3)).toEqual(5); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-react"], 3 | "rules": { 4 | "arrow-parens": [true, "ban-single-arg-parens"], 5 | "max-line-length": { 6 | "options": [80] 7 | }, 8 | "no-any": true, 9 | "quotemark": [true, "single", "jsx-double"], 10 | "no-implicit-dependencies": false, 11 | "semicolon": [true, "always"], 12 | "no-submodule-imports": false, 13 | "jsx-boolean-value": [true, "never"], 14 | "jsx-no-lambda": false, 15 | "no-angle-bracket-type-assertion": false, 16 | "no-console": false, 17 | "ordered-imports":false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | A React, Redux, TypeScript and Webpack starter 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "types": ["node"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": false, 8 | "noImplicitAny": false, 9 | "pretty": true, 10 | "removeComments": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "allowSyntheticDefaultImports": true, 14 | "target": "es6", 15 | "sourceMap": true, 16 | "jsx": "react", 17 | "allowJs": true, 18 | "outDir": "./dist", 19 | "typeRoots": ["types"] 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "src/**/*.spec.*"] 23 | } 24 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "build/*", 4 | "node_modules" 5 | ], 6 | "linters": { 7 | "{src,docs}/**/*.{ts,tsx}": [ 8 | "prettier --parser typescript --write", 9 | "tslint -c tslint.json --fix", 10 | "tslint -c tslint.json", 11 | "git add" 12 | ], 13 | "*.{css,scss}": [ 14 | "prettier --write", 15 | "stylelint -q -s scss --fix", 16 | "git add" 17 | ], 18 | "*.{png,jpeg,jpg,gif,svg}": [ 19 | "imagemin-lint-staged", 20 | "git add" 21 | ], 22 | "src/components/**/*.{ts,tsx}": [ 23 | "jest --findRelatedTests --config jest.config.js", 24 | "git add" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand" 13 | ], 14 | "cwd": "${workspaceFolder}", 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-idiomatic-order", 5 | "./node_modules/prettier-stylelint/config.js" 6 | ], 7 | "plugins": [ 8 | "stylelint-scss" 9 | ], 10 | "rules": { 11 | "max-nesting-depth": 5, 12 | "no-descending-specificity": null, 13 | "selector-list-comma-newline-after": "always-multi-line", 14 | "declaration-empty-line-before": "never", 15 | "declaration-colon-newline-after": null, 16 | "value-list-comma-newline-after": null, 17 | "font-family-no-missing-generic-family-keyword": null, 18 | "at-rule-no-unknown": null, 19 | "scss/at-rule-no-unknown": true 20 | }, 21 | "ignoreFiles": ["./node_modules/**", "**/*.ts", "**/*.tsx"] 22 | } 23 | -------------------------------------------------------------------------------- /src/store/configureStore.prod.ts: -------------------------------------------------------------------------------- 1 | import { init, model } from '@rematch/core'; 2 | import createHistory from 'history/createBrowserHistory'; 3 | import { routerMiddleware, routerReducer } from 'react-router-redux'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | 6 | import models from '../models'; 7 | 8 | export const history = createHistory(); 9 | 10 | const middlewares = [routerMiddleware(history), thunkMiddleware]; 11 | 12 | const configureStore = (initialState = {}) => { 13 | const store = init({ 14 | models, 15 | redux: { 16 | middlewares, 17 | reducers: { 18 | routing: routerReducer, 19 | ...initialState, 20 | }, 21 | }, 22 | }); 23 | 24 | return store; 25 | }; 26 | 27 | export default configureStore; 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | module.exports = { 3 | bail: true, 4 | verbose: true, 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 6 | testPathIgnorePatterns: ['/node_modules/'], 7 | testRegex: '.*\\.spec\\.(jsx?|tsx?)$', 8 | moduleDirectories: ['node_modules', 'shared'], 9 | moduleNameMapper: { 10 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 11 | '/jest/__mocks__/fileMock.js', 12 | '\\.(css|scss|less)$': 'identity-obj-proxy', 13 | }, 14 | transform: { 15 | '^.+\\.tsx?$': 'ts-jest', 16 | }, 17 | collectCoverageFrom: [ 18 | 'src/**/*.{ts,tsx}', 19 | 'src/**/**/*.{ts,tsx}', 20 | '!**/node_modules/**', 21 | '!**/**/*.d.ts', 22 | ], 23 | "coverageDirectory": "./coverage/", 24 | "collectCoverage": true 25 | }; 26 | -------------------------------------------------------------------------------- /jest/generator/createJestTest.js: -------------------------------------------------------------------------------- 1 | function createJestTest(filename) { 2 | const fileWithOutExt = filename.replace(/\.[^/.]+$/, ''); 3 | const readableFilename = capitalizeFirstLetter(fileWithOutExt); 4 | return `import * as Enzyme from 'enzyme'; 5 | import * as Adapter from 'enzyme-adapter-react-16'; 6 | import toJson from 'enzyme-to-json'; 7 | import * as React from 'react'; 8 | import ${readableFilename} from '../${readableFilename}'; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | describe('${readableFilename}', () => { 13 | test('Should render ${readableFilename} correctly', () => { 14 | const wrapper = Enzyme.shallow(<${readableFilename} />); 15 | expect(toJson(wrapper)).toMatchSnapshot(); 16 | }); 17 | }); 18 | `; 19 | } 20 | 21 | const capitalizeFirstLetter = string => 22 | string.charAt(0).toUpperCase() + string.slice(1); 23 | 24 | module.exports = { createJestTest }; 25 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10 11 | 12 | working_directory: ~/repo 13 | 14 | steps: 15 | - checkout 16 | 17 | # Download and cache dependencies 18 | - restore_cache: 19 | keys: 20 | - v1-dependencies-{{ checksum "package.json" }} 21 | # fallback to using the latest cache if no exact match is found 22 | - v1-dependencies- 23 | 24 | - run: yarn 25 | 26 | - save_cache: 27 | paths: 28 | - node_modules 29 | key: v1-dependencies-{{ checksum "package.json" }} 30 | 31 | # run tests 32 | - run: yarn test:coverage 33 | - run: yarn build -------------------------------------------------------------------------------- /config/server.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var nodeExternals = require('webpack-node-externals'); 5 | 6 | module.exports = { 7 | mode: 'production', 8 | 9 | target: 'node', 10 | 11 | externals: [nodeExternals()], 12 | 13 | context: path.resolve(__dirname, '..'), 14 | 15 | entry: { 16 | server: ['./src/server.ts'], 17 | }, 18 | 19 | output: { 20 | path: path.resolve(__dirname, '../dist'), 21 | filename: '[name].js', 22 | publicPath: '/', 23 | libraryTarget: 'commonjs2', 24 | }, 25 | 26 | resolve: { 27 | extensions: ['.json', '.ts', '.tsx', '.js'], 28 | modules: ['src', 'node_modules'], 29 | }, 30 | 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.tsx?$/, 35 | include: path.resolve(__dirname, '../src'), 36 | use: [ 37 | { 38 | loader: 'awesome-typescript-loader', 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kevin 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 | -------------------------------------------------------------------------------- /config/client.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const helpers = require('./helpers'); 4 | 5 | const isDev = process.env.NODE_ENV === 'development'; 6 | 7 | module.exports = { 8 | cache: isDev, 9 | 10 | target: 'web', 11 | 12 | devtool: isDev ? 'eval-source-map' : 'source-map', 13 | 14 | stats: { 15 | colors: isDev, 16 | reasons: isDev, 17 | errorDetails: isDev, 18 | }, 19 | 20 | context: path.resolve(__dirname, '..'), 21 | 22 | entry: { 23 | client: [ 24 | isDev && 'webpack-hot-middleware/client', 25 | './src/client.tsx', 26 | ].filter(Boolean), 27 | }, 28 | 29 | output: { 30 | path: path.resolve(__dirname, '../dist', 'public'), 31 | filename: isDev ? '[name].js' : '[name].[hash].js', 32 | publicPath: '/', 33 | }, 34 | 35 | resolve: { 36 | extensions: ['.json', '.ts', '.tsx', '.js'], 37 | modules: ['src', 'node_modules'], 38 | }, 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.tsx?$/, 44 | include: path.resolve(__dirname, '../src'), 45 | use: [ 46 | { 47 | loader: 'awesome-typescript-loader', 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | 54 | plugins: helpers.getPlugins(), 55 | }; 56 | -------------------------------------------------------------------------------- /jest/generator/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'), 4 | fs = require('fs'); 5 | const { generateFile } = require('./generateFile'); 6 | 7 | let counter = 0; 8 | function fromDir(startPath, fileExt) { 9 | if (!fs.existsSync(startPath)) { 10 | console.log('no dir ', startPath); 11 | return Promise.reject(); 12 | } 13 | 14 | const files = fs.readdirSync(startPath); 15 | return Promise.all( 16 | files.map(file => { 17 | const filename = file; 18 | const filePath = path.join(startPath, file); 19 | const stat = fs.lstatSync(filePath); 20 | if (stat.isDirectory()) { 21 | return fromDir(filePath, fileExt); //recurse 22 | } else if (filename.indexOf(fileExt) >= 0) { 23 | return generateFile(`./${filePath}`, filename, startPath).then( 24 | numberOfCreatedFiles => (counter = counter + numberOfCreatedFiles) 25 | ); 26 | } else { 27 | return Promise.resolve(); 28 | } 29 | }) 30 | ); 31 | } 32 | 33 | const projectPath = process.argv[2]; 34 | 35 | if (!projectPath) { 36 | throw Error('Not path given'); 37 | } 38 | const ext = process.argv[3] || '.tsx'; 39 | 40 | fromDir(projectPath, ext) 41 | .then(() => { 42 | console.log(`Finished, created ${counter} jest snapshot files`); 43 | }) 44 | .catch(err => console.log('jest-generator', err)); 45 | -------------------------------------------------------------------------------- /src/store/configureStore.dev.ts: -------------------------------------------------------------------------------- 1 | import { init, model } from '@rematch/core'; 2 | import createHistory from 'history/createBrowserHistory'; 3 | import { Iterable } from 'immutable'; 4 | import { routerMiddleware, routerReducer } from 'react-router-redux'; 5 | import { createLogger } from 'redux-logger'; 6 | import thunkMiddleware from 'redux-thunk'; 7 | 8 | import models from '../models'; 9 | 10 | // tslint:disable-next-line:no-any 11 | declare var module: { hot: any }; 12 | 13 | export const history = createHistory(); 14 | 15 | const middlewares = [ 16 | routerMiddleware(history), 17 | thunkMiddleware, 18 | createLogger({ 19 | collapsed: true, 20 | stateTransformer: state => { 21 | return Iterable.isIterable(state) ? state.toJS() : state; 22 | }, 23 | }), 24 | ]; 25 | 26 | const configureStore = (initialState = {}) => { 27 | const store = init({ 28 | models, 29 | redux: { 30 | middlewares, 31 | reducers: { 32 | routing: routerReducer, 33 | ...initialState, 34 | }, 35 | }, 36 | }); 37 | 38 | if (module.hot) { 39 | // Hot module replacement for reducers 40 | module.hot.accept('../models', () => { 41 | Object.keys(models).forEach(modelKey => { 42 | model({ 43 | name: modelKey, 44 | ...models[modelKey], 45 | }); 46 | }); 47 | }); 48 | } 49 | 50 | return store; 51 | }; 52 | 53 | export default configureStore; 54 | -------------------------------------------------------------------------------- /dist/server.js: -------------------------------------------------------------------------------- 1 | module.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=n(2),o=n(3),u=o();u.use(o.static("./dist/public")),u.get("*.js",(e,t,n)=>{e.url=e.url+".gz",t.set("Content-Encoding","gzip"),n()}),u.listen(8080,e=>{if(e)return console.log(r.default.red.bold(e));console.log(r.default.green.bold("\n\n##################################\n### App listening on port 8080 ###\n##################################"))})},function(e,t){e.exports=require("chalk")},function(e,t){e.exports=require("express")}]); -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 6 | const CompressionPlugin = require('compression-webpack-plugin'); 7 | 8 | const isDev = process.env.NODE_ENV === 'development'; 9 | 10 | module.exports = { 11 | getPlugins: () => { 12 | const plugins = [ 13 | new CopyWebpackPlugin([ 14 | { from: path.resolve(__dirname, '../src/favicon.svg') }, 15 | ]), 16 | 17 | new HtmlWebpackPlugin({ 18 | template: path.resolve(__dirname, '../src/index.html'), 19 | }), 20 | ]; 21 | 22 | if (isDev) { 23 | plugins.push( 24 | new webpack.HotModuleReplacementPlugin({ 25 | multiStep: false, // https://github.com/jantimon/html-webpack-plugin/issues/533 26 | }) 27 | ); 28 | } else { 29 | plugins.push( 30 | new UglifyJsPlugin({ 31 | uglifyOptions: { 32 | output: { 33 | beautify: false, 34 | }, 35 | }, 36 | sourceMap: true, 37 | }), 38 | 39 | new CompressionPlugin({ 40 | asset: '[path].gz[query]', 41 | algorithm: 'gzip', 42 | test: /\.js$|\.css$|\.html$/, 43 | threshold: 10240, 44 | minRatio: 0.8, 45 | }) 46 | ); 47 | } 48 | 49 | return plugins; 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-var-requires 2 | import chalk from 'chalk'; 3 | import * as express from 'express'; 4 | 5 | const app = express(); 6 | 7 | if (process.env.NODE_ENV !== 'production') { 8 | // Apply only in development mode 9 | const webpack = require('webpack'); 10 | const webpackConfig = require('../config/client.config.js'); 11 | 12 | const compiler = webpack({ 13 | mode: 'development', 14 | ...webpackConfig, 15 | }); 16 | 17 | const webpackDevMiddleware = require('webpack-dev-middleware')(compiler, { 18 | headers: { 19 | 'Access-Control-Allow-Origin': '*', 20 | }, 21 | noInfo: true, 22 | poll: true, 23 | publicPath: webpackConfig.output.publicPath, 24 | quiet: false, 25 | reload: true, 26 | stats: { colors: true }, 27 | }); 28 | 29 | const webpackHotMiddleware = require('webpack-hot-middleware')(compiler, { 30 | reload: true, 31 | }); 32 | 33 | app.use(webpackDevMiddleware); 34 | app.use(webpackHotMiddleware); 35 | } else { 36 | // Apply only in production mode 37 | app.use(express.static('./dist/public')); 38 | 39 | app.get('*.js', (req, res, next) => { 40 | req.url = req.url + '.gz'; 41 | res.set('Content-Encoding', 'gzip'); 42 | next(); 43 | }); 44 | } 45 | 46 | app.listen(8080, err => { 47 | if (err) { 48 | // tslint:disable-next-line:no-console 49 | return console.log(chalk.red.bold(err)); 50 | } 51 | 52 | // tslint:disable-next-line:no-console 53 | console.log( 54 | chalk.green.bold( 55 | '\n\n' + 56 | '##################################\n' + 57 | '### App listening on port 8080 ###\n' + 58 | '##################################', 59 | ), 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /jest/generator/generateFile.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require('mkdirp'), 2 | createJestFile = require('./createJestTest'), 3 | fs = require('fs'); 4 | 5 | const generateFile = (filePath, filename, startPath) => { 6 | return new Promise((resolve, reject) => { 7 | checkIfReactFile(filePath) 8 | .then(({ isReactFile, filePath }) => { 9 | if (isReactFile) { 10 | const testPath = `${startPath}/__tests__`; 11 | const testFileName = `${filename.replace(/\.[^/.]+$/, '')}.spec.tsx`; 12 | const fileContent = createJestFile.createJestTest(filename); 13 | 14 | resolve( 15 | findTestFolder(testPath).then(() => 16 | createTestFile(`${testPath}/${testFileName}`, fileContent) 17 | ) 18 | ); 19 | } else { 20 | resolve(0); 21 | } 22 | }) 23 | .catch(reject); 24 | }); 25 | }; 26 | 27 | function createTestFile(filename, content) { 28 | return new Promise((resolve, reject) => { 29 | fs.open(filename, 'r', function(err, fd) { 30 | if (err) { 31 | fs.writeFile(filename, content, function(err) { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | resolve(1); 36 | } 37 | }); 38 | } else { 39 | console.log('The file exists!', filename); 40 | resolve(0); 41 | } 42 | }); 43 | }); 44 | } 45 | 46 | function findTestFolder(path) { 47 | return new Promise((resolve, reject) => { 48 | mkdirp(path, function(err) { 49 | if (err) { 50 | reject(err); 51 | } else { 52 | // console.log(`created test folder: ${path}`); 53 | resolve(); 54 | } 55 | }); 56 | }); 57 | } 58 | 59 | function checkIfReactFile(filePath) { 60 | if (filePath.indexOf('.spec.tsx') > 0 || filePath.indexOf('.test.tsx') > 0) { 61 | // Don't generate files for test files 62 | return Promise.resolve(false); 63 | } 64 | 65 | return new Promise((resolve, reject) => { 66 | fs.readFile(filePath, function(err1, data) { 67 | if (err1) { 68 | console.error(err1); 69 | reject(); 70 | } else { 71 | const res = { 72 | filePath, 73 | isReactFile: isReactFile(data), 74 | }; 75 | resolve(res); 76 | } 77 | }); 78 | }); 79 | } 80 | 81 | function isReactFile(data) { 82 | // naive way of trying to check if the file is a React comp 83 | if ( 84 | data.indexOf(`require('react')`) >= 0 || 85 | data.indexOf(`from 'react'`) >= 0 86 | ) { 87 | return true; 88 | } else { 89 | return false; 90 | } 91 | } 92 | 93 | module.exports = { generateFile }; 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "defensor-automated-testing", 4 | "version": "0.1.3", 5 | "description": "A React, Redux, TypeScript and Webpack starter", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/TingGe/defensor-automated-testing.git" 10 | }, 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "start": "yarn run start:dev", 14 | "start:dev": "cross-env NODE_ENV=development ts-node src/server.ts", 15 | "start:prod": "yarn run build && node dist/server.js", 16 | "build": "yarn run clean && yarn run build:server && yarn run build:prod", 17 | "build:server": "cross-env webpack --progress --colors --config config/server.config.js", 18 | "build:prod": "cross-env webpack --progress --colors --mode production --config config/client.config.js", 19 | "lint": "tslint --project tsconfig.json", 20 | "prebuild": "npm test", 21 | "lint:fix": "prettier --write './src/**/*.{ts,tsx,js,scss}' && tslint -c tslint.json --fix './src/**/*.{js,ts,tsx}'", 22 | "test": "jest", 23 | "test:coverage": "jest --coverage && codecov", 24 | "test:createTests": "node jest/generator/index.js src/components", 25 | "test:update-snapshot": "jest -u", 26 | "test:generate-output": "jest --json --outputFile=.jest-test-results.json || true", 27 | "precommit": "lint-staged", 28 | "test:watch": "npm run test:generate-output -- --watch", 29 | "commitmsg": "commitlint -E GIT_PARAMS", 30 | "prebuild:storybook": "npm run test:generate-output", 31 | "build:storybook": "build-storybook -c .storybook -o build/", 32 | "predeploy": "npm run build:storybook", 33 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md" 34 | }, 35 | "devDependencies": { 36 | "@commitlint/cli": "^7.1.1", 37 | "@commitlint/config-conventional": "^7.1.1", 38 | "@types/enzyme": "^3.1.13", 39 | "@types/enzyme-adapter-react-16": "^1.0.3", 40 | "@types/enzyme-to-json": "^1.5.2", 41 | "@types/jest": "^23.3.1", 42 | "@types/react": "^16.4.11", 43 | "@types/react-dom": "^16.0.7", 44 | "@types/react-hot-loader": "^4.1.0", 45 | "@types/react-router-redux": "^5.0.15", 46 | "@types/redux-immutable": "^3.0.38", 47 | "@types/webpack": "^4.4.10", 48 | "@types/webpack-env": "^1.13.6", 49 | "awesome-typescript-loader": "^5.2.0", 50 | "codecov": "^3.0.4", 51 | "compression-webpack-plugin": "^1.1.11", 52 | "conventional-changelog-cli": "^2.0.5", 53 | "copy-webpack-plugin": "^4.5.2", 54 | "cross-env": "^5.2.0", 55 | "enzyme": "^3.4.4", 56 | "enzyme-adapter-react-16": "^1.2.0", 57 | "enzyme-to-json": "^3.3.4", 58 | "html-webpack-plugin": "^3.2.0", 59 | "husky": "^0.14.3", 60 | "identity-obj-proxy": "^3.0.0", 61 | "imagemin-lint-staged": "^0.3.0", 62 | "jest": "^23.5.0", 63 | "lint-staged": "^7.2.2", 64 | "nyc": "^12.0.2", 65 | "prettier": "^1.14.2", 66 | "prettier-stylelint": "^0.4.2", 67 | "rimraf": "^2.6.2", 68 | "stylelint-config-idiomatic-order": "^5.0.0", 69 | "stylelint-config-standard": "^18.2.0", 70 | "stylelint-scss": "^3.3.0", 71 | "ts-jest": "^23.1.4", 72 | "ts-node": "^7.0.1", 73 | "tslint": "^5.11.0", 74 | "tslint-react": "^3.6.0", 75 | "typescript": "^3.0.1", 76 | "uglifyjs-webpack-plugin": "^1.3.0", 77 | "webpack": "^4.17.1", 78 | "webpack-cli": "^3.1.0", 79 | "webpack-dev-middleware": "^3.1.3", 80 | "webpack-hot-middleware": "^2.22.3", 81 | "webpack-node-externals": "^1.7.2" 82 | }, 83 | "dependencies": { 84 | "@rematch/core": "^0.6.0", 85 | "chalk": "^2.4.1", 86 | "express": "^4.16.3", 87 | "history": "^4.7.2", 88 | "immutable": "^3.8.2", 89 | "react": "^16.4.2", 90 | "react-dom": "^16.4.2", 91 | "react-hot-loader": "^4.3.4", 92 | "react-redux": "^4.4.9", 93 | "react-router-dom": "^4.3.1", 94 | "react-router-redux": "^5.0.0-alpha.9", 95 | "redux-logger": "^3.0.6", 96 | "redux-thunk": "2.2.0" 97 | }, 98 | "husky": { 99 | "hooks": { 100 | "pre-commit": "lint-staged" 101 | } 102 | }, 103 | "author": "TingGe<505253293@163.com>", 104 | "license": "MIT" 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux 前端研发品质实践 2 | 3 | [![codebeat badge](https://codebeat.co/badges/7bd2dadc-c77b-4db9-9790-e8a5f83a8dc8)](https://codebeat.co/projects/github-com-tingge-defensor-automated-testing-master) [![codecov](https://codecov.io/gh/TingGe/defensor-automated-testing/branch/master/graph/badge.svg)](https://codecov.io/gh/TingGe/defensor-automated-testing) [![CircleCI](https://circleci.com/gh/TingGe/defensor-automated-testing.svg?style=svg)](https://circleci.com/gh/TingGe/defensor-automated-testing) 4 | 5 | ![](https://raw.githubusercontent.com/TingGe/defensor-automated-testing/master/assets/image/testing-structure.png) 6 | 7 | > 最佳适用于 `TypeScript + Scss/Less + React + Redux + React Dom + React Router + React Thunk` 技术栈的前端。 8 | > 9 | > 软件质量测量是对一系列可描述软件特性的属性值进行加权归一化的定量过程。 10 | 11 | 一个 React Redux 项目的模版项目。 12 | 13 | - 采用 `TypeScript + Scss/Less + React + Redux + React Dom + React Router + React Thunk` 技术栈; 14 | - 代码静态审查:husky + lint-staged + tslint + prettier + stylelint + imagemin-lint-staged; 15 | - 测试包括:单元测试、覆盖率测试、接入集成测试服务、e2e 测试和 watch 模式,husky + lint-staged + jest。 16 | 17 | ## Git 规范化注解 18 | 19 | > #### Commit 规范作用 20 | > 21 | > 1.提供更多的信息,方便排查与回退 22 | > 2.过滤关键字,迅速定位 23 | > 3.方便生成文档 24 | 25 | ### 规范 26 | 27 | ```git 28 | (): 29 | ``` 30 | 31 | - type 用于说明 `commit` 类别,只允许使用下面7个标识。 32 | 33 | ```git 34 | feat:新功能(feature) 35 | fix:修补bug 36 | docs:文档(documentation) 37 | style: 格式(不影响代码运行的变动) 38 | refactor:重构(即不是新增功能,也不是修改bug的代码变动) 39 | test:增加测试 40 | chore:构建过程或辅助工具的变动 41 | ``` 42 | 43 | - scope 用于说明 `commit` 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。 44 | 45 | - subject 是 `commit` 目的的简短描述,不超过50个字符。 46 | 47 | ```git 48 | 1.以动词开头,使用第一人称现在时,比如change,而不是changed或changes 49 | 2.第一个字母小写 50 | 3.结尾不加句号(.) 51 | ``` 52 | 53 | ### 执行方式 54 | 55 | - 校验 commit 规范:借助 [husky](https://github.com/typicode/husky) 在 commit 时自动校验。 56 | - 生成 Change log:` npm version [patch|minor|major]` ,可自动更新 CHANGELOG.md 57 | 58 | ## 代码静态审查 59 | 60 | 1. Git hook:husky + lint-staged 61 | 2. ts 和 tsx 合规检查和修复:tslint + prettier 62 | 3. scss 和 css 合规检查和修复:stylelint 63 | 4. 图片和 svg 等压缩:imagemin-lint-staged 64 | 65 | ### prettier 执行方式 66 | 67 | 方式一:VS Code 的 [prettier-vscode 插件](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)提示 68 | 69 | 方式二:借助 [husky](https://github.com/typicode/husky) 在代码 commit 时代码审查(自动修复和提示) 70 | 71 | 方式三:根目录执行以下命令(自动修复和提示) 72 | 73 | ```bash 74 | npx prettier --write './src/**/*.{ts,tsx,js,scss}' 75 | ``` 76 | 77 | ### tslint 执行方式 78 | 79 | 方式一:VS Code 的 [vscode-tslint 插件](https://marketplace.visualstudio.com/items?itemName=eg2.tslint)提示 80 | 81 | 方式二:借助 [husky](https://github.com/typicode/husky) 在代码 commit 时代码审查(自动修复和提示) 82 | 83 | 方式三:根目录执行以下命令(自动修复和提示) 84 | 85 | ```bash 86 | tslint -c tslint.json --fix './src/**/*.{js,ts,tsx}' 87 | ``` 88 | 89 | ### stylelint 执行方式 90 | 91 | 方式一:VS Code 的 [stylelint 插件](https://marketplace.visualstudio.com/items?itemName=shinnn.stylelint)提示 92 | 93 | 方式二:借助 [husky](https://github.com/typicode/husky) 在代码 commit 时代码审查(自动修复和提示) 94 | 95 | 方式三:根目录执行以下命令(自动修复和提示) 96 | 97 | ```Bash 98 | npx stylelint -s scss --fix --stdin-filename ./(src|docs)/**/*.scss 99 | ``` 100 | 101 | ## 测试 102 | 103 | ### 关于是否需要自动化测试? 104 | 105 | 自动化测试的长远价值高于手工,所以如果自动化的性价比已经开始高于手工,就可以着手去做。项目后期和维护期,自动化介入为回归测试做准备,可以最大化自动化收益。 106 | 107 | 参考价值公式 108 | 109 | - 自动化收益 = 迭代次数 * 全手动执行成本 - 首次自动化成本 - 维护次数 * 维护成本 110 | 111 | 本项目采用的自动化测试技术方案 112 | 113 | 1. React Redux 测试:Typescript + Jest + Enzyme 组合 114 | 2. 集成测试: [Defensor E2E Testing](https://github.com/TingGe/defensor-e2e-testing) 115 | 116 | 117 | ### 组件测试:Typescript + Jest + Enzyme 组合 118 | 119 | 1. 支持 watch 模式 120 | 2. actions 测试 121 | 3. reducer 测试 122 | 4. select 测试 123 | 5. React + Redux 测试 124 | 6. 覆盖率和输出报告 125 | 126 | ### E2E 测试:Defensor E2E Testing 127 | 128 | > 可独立于项目代码。支持本地运行、手工触发、定时触发、发布流程触发四种方式,实现业务逻辑的持续测试。 129 | 130 | 1. 跨端(多浏览器兼容)自动化测试及报告: [UI Recorder](https://github.com/alibaba/uirecorder)、[F2etest](https://github.com/alibaba/f2etest) 131 | 2. 测试脚本:测试代码的 Github 仓库 132 | 3. 用例、测试计划、任务分派和缺陷管理:Aone 133 | 4. 持续集成(CI)服务:Aone 实验室 CISE 134 | 5. 全球化(G11N)自动测试报告:ACGT 135 | 6. 测试简报、测试计划进度跟踪、待修复缺陷跟踪:OneShot 截屏服务/爬虫服务 + 钉钉群机器人 136 | 7. 容器化: Docker/Kubernetes 编排技术实现的 Selenium Grid 137 | 8. 徽章服务:Aone badge 138 | 9. 多环境管理和健康大盘Chrome扩展:[defensor-multi-environment-manager](https://github.com/TingGe/defensor-multi-environment-manager) 139 | 10. 线上巡检:(可配合线上监控系统和报告数据实现可视化) 140 | 141 | ## 其他辅助工具 142 | 143 | 1. 快速应用 CLI 工具:[defensor-cli](https://github.com/TingGe/defensor-cli) 144 | 2. 命令行工具,主要用于 Newsletter 等群发通知:[defensor-node-cli-broadcast](https://github.com/TingGe/defensor-node-cli-broadcast) 145 | 146 | ## 对比的一些工具 147 | 148 | - Jest:[Create React App](https://github.com/facebookincubator/create-react-app) 、 [Microsoft/TypeScript-React-Starter](Microsoft/TypeScript-React-Starter) 和 [Ant Design](https://github.com/ant-design/ant-design-pro) 中推荐方案,内置断言、测试覆盖率工具,是个一体化方案、开箱即用。提供测试环境Dom API支持、合理的默认值、预处理代码和默认执行并行测试在内的特性; 149 | - AVA: 相对于 Mocha 执行更快,测试环境隔离、支持原子测试,相对于 Jest 组合更加灵活但最佳实践的开发工具、生态链支持稍有欠缺; 150 | - Mocha + Chai:相对较为成熟。 151 | 152 | 153 | 154 | 项目接入持续集成在多人开发同一个仓库时候能起到很大的用途,每次push都能自动触发测试,测试没过会发生告警。 155 | 156 | 如果需求采用 Issues+Merge Request 来管理,每个需求一个Issue + 一个分支,开发完成后提交 Merge Request ,由项目 Owner 负责合并,项目质量将更有保障。 157 | 158 | GITHUB上的小工具,大概分成这么几类:代码质量、持续集成、依赖管理、本地化、监控、项目管理和安全等。 159 | 160 | | 级别 | 类别 | 作用 | 选型 | 同类 | 161 | | ------- | ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | 162 | | - | 静态代码审查 | 统一团队代码风格 | [Prettier](https://github.com/prettier/prettier) | - | 163 | | - | 静态代码审查 | 现代 CSS 格式验证工具 | [Stylelint](https://github.com/stylelint/stylelint) | - | 164 | | - | 静态代码审查 | TypeScript 格式验证工具 | [Tslint](https://palantir.github.io/tslint/) | - | 165 | | - | 静态代码审查 | 安全审计,依赖项跟踪 | npm audit fix | [jj](https://github.com/greenkeeperio/greenkeeper), [Libraries.io](https://github.com/librariesio/libraries.io) | 166 | | - | 静态代码审查 | 可访问性、性能和安全的开源检查(Linting)工具 | - | [Webhint](https://github.com/webhintio/hint) | 167 | | - | 代码质量管理平台 | 集成不同的测试工具,代码分析工具,持续集成工具等。自动 Code Review 辅助 | [SonarQube](https://github.com/SonarSource/sonarqube) + [SonarLint](https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode) | [CodeBeat](https://codebeat.co/), [Codacy](https://github.com/codacy), [Code Climat](https://github.com/codeclimate/codeclimate) | 168 | | 单元 | 测试框架 | test runner, snapshots, display, and watch | [Jest](https://jestjs.io/) 内置的 Jasmine | [AVA](https://github.com/avajs/ava), Mocha, Wallaby.js, | 169 | | 单元 | 断言库 | assertions functions | [enzyme](https://github.com/airbnb/enzyme) + Jest 的 Matchers | [Unexpected](https://github.com/unexpectedjs/unexpected), Chai, | 170 | | 单元 | Mock工具 | mocks, spies, and stubs | Jest 的 Mock Functions | [testdouble.js](https://github.com/testdouble/testdouble.js), [sinon](http://sinonjs.org/), | 171 | | 单元 | 测试覆盖率工具 | code coverage | Jest 内置的 Istanbul + [Codecov](https://codecov.io/) | [Coveralls](https://coveralls.io/), [nyc](https://github.com/istanbuljs/nyc) | 172 | | 单元 | 模拟工具 | 模拟浏览器 dom | Jest 内置的 JSDOM | [JsDom](https://github.com/jsdom/jsdom) | 173 | | - | Git 规范化注解向导工具 | Commit 规范,生成 Change log | [commitlint](https://github.com/marionebl/commitlint) + [conventional-changelog](https://github.com/conventional-changelog) | [commitizen](https://github.com/commitizen/cz-cli), [semantic-release](https://github.com/semantic-release/semantic-release) | 174 | | - | - | 与 Storybook 集成 | - | - | 175 | | - | - | 持续集成服务 | [CircleCI](https://circleci.com/) | [Jenkins](https://jenkins.io/), [Travis](https://travis-ci.org/), [Hound](https://houndci.com/) | 176 | | 端到端 | | e2e | [Defensor E2E Testing](https://github.com/TingGe/defensor-e2e-testing) | [Cypress](https://www.cypress.io/), [Nightwatch](http://nightwatchjs.org/), [Protractor](http://www.protractortest.org/), [Casper](http://casperjs.org/), [testcafe](https://github.com/DevExpress/testcafe), [DalekJS](https://github.com/dalekjs), [testwise-recorder](https://github.com/testwisely/testwise-recorder),[Puppeteer Recorder](https://github.com/checkly/puppeteer-recorder) + [Puppeteer](https://github.com/GoogleChrome/puppeteer) | 177 | | - | - | - | - | - | 178 | | ChatOps | 自动化运维 | 查看各项指标;自动发布;发布报告等 | [钉钉机器人](https://open-doc.dingtalk.com/docs/doc.htm?treeId=257&articleId=105735&docType=1) | Lita,Err, [Hubot](https://hubot.github.com/) | 179 | | - | 合规审查 | 自动追踪开源代码的授权许可协议;开源代码合规化 | [Fossa](https://fossa.io/) | - | 180 | 181 | ## 最佳实践 182 | 183 | - 通过 `npm run test:createTests` ,批量自动化生成单元测试代码 184 | 185 | 186 | ## 踩过的坑 187 | 188 | - package.json 中包依赖版本锁定管理:不要忽略 warning,关注 [Enzyme Working with React 16](http://airbnb.io/enzyme/docs/installation/react-16.html) 等配置文档 189 | - ignore-styles 忽略样式和资源文件:需要 hook node 的 require, 因此将 setup.ts 改成 setup.js 190 | 191 | ### API Docs 192 | 193 | - Enzyme: https://github.com/airbnb/enzyme/tree/master/docs/api 194 | - Sinon:http://sinonjs.org/releases/v4.1.2/ 195 | 196 | ## 参考 197 | 198 | - [Wings-让单元测试智能全自动生成](http://www.threadingtest.com/newstest/Wings%E5%8F%91%E5%B8%83-%E8%AE%A9%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E6%99%BA%E8%83%BD%E5%85%A8%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90.html) 199 | - [议题解读《我的Web应用安全模糊测试之路》](https://www.anquanke.com/post/id/152729) 200 | - [开发要不要自己做测试?怎么做?](https://mp.weixin.qq.com/s?__biz=MzIzNjUxMzk2NQ==&mid=2247489501&idx=2&sn=fb233a9dcedbecb385cc828f2117b657&chksm=e8d7e81fdfa061098b6d4ec40d6a8aa63395b25af681fcf0faf9f2519bfeed053dc4a80bb124&scene=27#wechat_redirect) 201 | 202 | 203 | - [入门:前端自动化测试karma,Backstopjs,Selenium-webdriver,Moch](https://juejin.im/post/5b13526d6fb9a01e831461e6) 204 | 205 | - [代码自动化扫描系统的建设](https://www.anquanke.com/post/id/158929) 206 | - [你可能会忽略的 Git 提交规范](http://jartto.wang/2018/07/08/git-commit/) 207 | - [使用Jest进行React单元测试](https://www.codetd.com/article/2675508) 208 | - [聊聊前端开发的测试](https://www.diycode.cc/topics/716) 209 | - [如何进行前端自动化测试?](https://www.zhihu.com/question/29922082/answer/46141819) 210 | - [JavaScript 单元测试框架大乱斗:Jasmine、Mocha、AVA、Tape 以及 Jest](https://raygun.com/blog/javascript-unit-testing-frameworks/) 211 | - [基于 JavaScript 的 Web 应用的端到端测试工具对比](https://mo.github.io/2017/07/20/javascript-e2e-integration-testing.html) 212 | - [别再加端到端集成测试了,快换契约测试吧](http://insights.thoughtworks.cn/contract-test/) 213 | - [从工程化角度讨论如何快速构建可靠React组件](https://github.com/lcxfs1991/blog/issues/18) 214 | - [How to Test a React and Redux Application ](https://semaphoreci.com/community/tutorials/getting-started-with-create-react-app-and-ava) 215 | - [How to prevent “Property '…' does not exist on type 'Global'” with jsdom and typescript?](https://stackoverflow.com/questions/40743131/how-to-prevent-property-does-not-exist-on-type-global-with-jsdom-and-t) 216 | - [Using enzyme with JSDOM](http://airbnb.io/enzyme/docs/guides/jsdom.html) 217 | - [Antd Pro UI Test](https://pro.ant.design/docs/ui-test#单元测试) 218 | - [Automated React Component Testing with Jest](https://www.distelli.com/docs/tutorials/test-your-react-component-with-jest/) 219 | 220 | ## 重点解答 221 | 222 | 1. 常用组合和现在组合优缺点; 223 | 2. 各组合适用的应用场景; 224 | 3. 测试的开发体验。 225 | 226 | ## 未来的可能 227 | 228 | 1. 与测试团队整体测试的接入; 229 | 2. 对开发者更加友好,降低用例的创建和维护成本; 230 | 3. 从投入产出角度,减少人工干预环节。 231 | 232 | ## 许可 License 233 | 234 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FTingGe%2Fdefensor-automated-testing.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FTingGe%2Fdefensor-automated-testing?ref=badge_large) 235 | -------------------------------------------------------------------------------- /dist/public/client.7f1954cbc82189d2016b.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=24)}([function(e,t,n){e.exports=n(34)()},function(e,t,n){"use strict";e.exports=n(26)},function(e,t,n){"use strict";e.exports=function(e,t,n,r,o,i,a,u){if(!e){var l;if(void 0===t)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,i,a,u],s=0;(l=new Error(t.replace(/%s/g,function(){return c[s++]}))).name="Invariant Violation"}throw l.framesToPop=1,l}}},function(e,t,n){"use strict";e.exports=function(){}},function(e,t,n){"use strict";e.exports=function(){}},function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";var r=n(3),o=n.n(r),i=n(2),a=n.n(i),u=n(1),l=n.n(u),c=n(0),s=n.n(c),f=Object.assign||function(e){for(var t=1;t may have only one child element"),this.unlisten=r.listen(function(){e.setState({match:e.computeMatch(r.location.pathname)})})},t.prototype.componentWillReceiveProps=function(e){o()(this.props.history===e.history,"You cannot change ")},t.prototype.componentWillUnmount=function(){this.unlisten()},t.prototype.render=function(){var e=this.props.children;return e?l.a.Children.only(e):null},t}(l.a.Component);d.propTypes={history:s.a.object.isRequired,children:s.a.node},d.contextTypes={router:s.a.object},d.childContextTypes={router:s.a.object.isRequired},t.a=d},function(e,t,n){"use strict";var r=n(8),o=n.n(r),i={},a=0;t.a=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments[2];"string"==typeof t&&(t={path:t});var r=t,u=r.path,l=r.exact,c=void 0!==l&&l,s=r.strict,f=void 0!==s&&s,p=r.sensitive,d=void 0!==p&&p;if(null==u)return n;var h=function(e,t){var n=""+t.end+t.strict+t.sensitive,r=i[n]||(i[n]={});if(r[e])return r[e];var u=[],l={re:o()(e,u,t),keys:u};return a<1e4&&(r[e]=l,a++),l}(u,{end:c,strict:f,sensitive:d}),y=h.re,m=h.keys,v=y.exec(e);if(!v)return null;var b=v[0],g=v.slice(1),w=e===b;return c&&!w?null:{path:u,url:"/"===u&&""===b?"/":b,isExact:w,params:m.reduce(function(e,t,n){return e[t.name]=g[n],e},{})}}},function(e,t,n){var r=n(50);e.exports=function e(t,n,o){return r(n)||(o=n||o,n=[]),o=o||{},t instanceof RegExp?function(e,t){var n=e.source.match(/\((?!\?)/g);if(n)for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:"",n=e&&e.split("/")||[],i=t&&t.split("/")||[],a=e&&r(e),u=t&&r(t),l=a||u;if(e&&r(e)?i=n:n.length&&(i.pop(),i=i.concat(n)),!i.length)return"/";var c=void 0;if(i.length){var s=i[i.length-1];c="."===s||".."===s||""===s}else c=!1;for(var f=0,p=i.length;p>=0;p--){var d=i[p];"."===d?o(i,p):".."===d?(o(i,p),f++):f&&(o(i,p),f--)}if(!l)for(;f--;f)i.unshift("..");!l||""===i[0]||i[0]&&r(i[0])||i.unshift("");var h=i.join("/");return c&&"/"!==h.substr(-1)&&(h+="/"),h}},function(e,t,n){"use strict";n.r(t);var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.default=function e(t,n){if(t===n)return!0;if(null==t||null==n)return!1;if(Array.isArray(t))return Array.isArray(n)&&t.length===n.length&&t.every(function(t,r){return e(t,n[r])});var o=void 0===t?"undefined":r(t);if(o!==(void 0===n?"undefined":r(n)))return!1;if("object"===o){var i=t.valueOf(),a=n.valueOf();if(i!==t||a!==n)return e(i,a);var u=Object.keys(t),l=Object.keys(n);return u.length===l.length&&u.every(function(r){return e(t[r],n[r])})}return!1}},function(e,t,n){"use strict";(function(e,r){var o,i=n(21);o="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==e?e:r;var a=Object(i.a)(o);t.a=a}).call(this,n(5),n(39)(e))},function(e,t,n){"use strict";var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,a,u=function(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}(e),l=1;l0&&void 0!==arguments[0]?arguments[0]:s,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.type,r=t.payload;return n===c?l({},e,{location:r}):e}function p(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}var d=function(e){function t(){var n,r;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t);for(var o=arguments.length,i=Array(o),a=0;aS.length&&S.push(e)}function R(e,t,n){return null==e?0:function e(t,n,r,o){var u=typeof t;"undefined"!==u&&"boolean"!==u||(t=null);var l=!1;if(null===t)l=!0;else switch(u){case"string":case"number":l=!0;break;case"object":switch(t.$$typeof){case i:case a:l=!0}}if(l)return r(o,t,""===n?"."+M(t,0):n),1;if(l=0,n=""===n?".":n+":",Array.isArray(t))for(var c=0;cthis.eventPool.length&&this.eventPool.push(e)}function de(e){e.eventPool=[],e.getPooled=fe,e.release=pe}o(se.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=le)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=le)},persist:function(){this.isPersistent=le},isPersistent:ce,destructor:function(){var e,t=this.constructor.Interface;for(e in t)this[e]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null,this.isPropagationStopped=this.isDefaultPrevented=ce,this._dispatchInstances=this._dispatchListeners=null}}),se.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null},se.extend=function(e){function t(){}function n(){return r.apply(this,arguments)}var r=this;t.prototype=r.prototype;var i=new t;return o(i,n.prototype),n.prototype=i,n.prototype.constructor=n,n.Interface=o({},r.Interface,e),n.extend=r.extend,de(n),n},de(se);var he=se.extend({data:null}),ye=se.extend({data:null}),me=[9,13,27,32],ve=Y&&"CompositionEvent"in window,be=null;Y&&"documentMode"in document&&(be=document.documentMode);var ge=Y&&"TextEvent"in window&&!be,we=Y&&(!ve||be&&8=be),xe=String.fromCharCode(32),ke={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},_e=!1;function Oe(e,t){switch(e){case"keyup":return-1!==me.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"blur":return!0;default:return!1}}function Pe(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var Ee=!1,Te={eventTypes:ke,extractEvents:function(e,t,n,r){var o=void 0,i=void 0;if(ve)e:{switch(e){case"compositionstart":o=ke.compositionStart;break e;case"compositionend":o=ke.compositionEnd;break e;case"compositionupdate":o=ke.compositionUpdate;break e}o=void 0}else Ee?Oe(e,n)&&(o=ke.compositionEnd):"keydown"===e&&229===n.keyCode&&(o=ke.compositionStart);return o?(we&&"ko"!==n.locale&&(Ee||o!==ke.compositionStart?o===ke.compositionEnd&&Ee&&(i=ue()):(ie="value"in(oe=r)?oe.value:oe.textContent,Ee=!0)),o=he.getPooled(o,t,n,r),i?o.data=i:null!==(i=Pe(n))&&(o.data=i),$(o),i=o):i=null,(e=ge?function(e,t){switch(e){case"compositionend":return Pe(t);case"keypress":return 32!==t.which?null:(_e=!0,xe);case"textInput":return(e=t.data)===xe&&_e?null:e;default:return null}}(e,n):function(e,t){if(Ee)return"compositionend"===e||!ve&&Oe(e,t)?(e=ue(),ae=ie=oe=null,Ee=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1