UNPKG

15.8 kBMarkdownView Raw
1# SPDT - Storybook Puppeteer Declarative Testing
2<p>
3 <a href="https://app.codeship.com/projects/329679">
4 <img src="https://app.codeship.com/projects/68070880-222e-0137-f223-6a5a7fbcefce/status?branch=master"
5 alt="Codeship Status for sseletskyy/storybook-puppeteer-declarative-testing">
6 </a>
7 <a href='https://coveralls.io/github/sseletskyy/storybook-puppeteer-declarative-testing?branch=master'><img src='https://coveralls.io/repos/github/sseletskyy/storybook-puppeteer-declarative-testing/badge.svg?branch=master' alt='Coverage Status' /></a>
8 <a href="https://www.npmjs.com/package/spdt">
9 <img src="https://img.shields.io/npm/v/spdt.svg"
10 alt="npm version">
11 </a>
12 <a href="https://packagephobia.now.sh/result?p=spdt">
13 <img src="https://packagephobia.now.sh/badge?p=spdt"
14 alt="install size">
15 </a>
16 <a href="https://github.com/sseletskyy/storybook-puppeteer-declarative-testing/blob/master/LICENSE.md">
17 <img src="https://img.shields.io/npm/l/spdt.svg"
18 alt="license">
19 </a>
20 <a href="https://david-dm.org/sseletskyy/storybook-puppeteer-declarative-testing">
21 <img src="https://david-dm.org/sseletskyy/storybook-puppeteer-declarative-testing/status.svg"
22 alt="dependency status">
23 </a>
24</p>
25
26## TL;DR
27
28```npm i -D spdt```
29or ```yarn add spdt -D```
30
31# Overview
32Declarative testing of isolated React components using storybook (v4) as a renderer and puppeteer+jest as a test runner.
33
34It's not yet another testing framework. It's just the way how to implement DRY (don't repeat yourself) principle for testing React components.
35
36* write a fixture - props for React component
37* add a declaration - a single line which describes what to test
38* write a simple storybook story
39* call **npm run spdt** to generate tests and run storybook server
40* call **npm run spdt:test** to run generated jest tests - puppeteer reads react components in storybook and calls assertions
41
42Here is an example.
43
44We have a React component to display a list of products.
45```javascript
46// file: src/example/ProductList.jsx
47const ProductList = ({list}) => (
48 <div>
49 {list.map((item, index) => <div key={index} className="itemElement">{item}</div>)}
50 </div>
51)
52export default ProductList
53```
54We have some testing data (fixture) to render: `['apricot', 'banana', 'carrot']`
55We need to write a test to check that three items (with class .itemElement) will be rendered.
56Let's write a fixture in such a form:
57```javascript
58// file: src/example/__tests__/ProductList.fixture.js
59export default {
60 listOfThree: {
61 props: {
62 items: ['apricot', 'banana', 'carrot']
63 },
64 spdt: {
65 checkSelector: {selector: '.itemElement', length: 3} // <----
66 }
67 }
68}
69```
70That simple declaration `checkSelector: {selector: '.itemElement', length: 3}` makes **spdt** library to generate a test for you.
71No need to copy-paste unit tests any more.
72Just write a test pattern (declaration) once and reuse it over and over (see example **testH1** below)
73
74You can add as many fixtures as you want:
75```javascript
76// file: src/example/__tests__/ProductList.fixture.js
77export default {
78 listOfThree: {
79 props: {
80 items: ['apricot', 'banana', 'carrot']
81 },
82 spdt: {
83 checkSelector: {selector: '.itemElement', length: 3} // <----
84 }
85 },
86 listOfTwo: {
87 props: {
88 items: ['orange', 'tangerine']
89 },
90 spdt: {
91 checkSelector: {selector: '.itemElement', length: 2} // <----
92 }
93 }
94}
95```
96
97
98The idea behind this module was to make testing of React+D3 components based on fixtures.
99However **spdt** can speed up testing of any React application
100
101## Here is a short description of the workflow:
102
103* create a React component, e.g `Comment.js`
104* create a fixture file with a set of properties for your component: `Comment.fixture.js`
105* create a story file `Comment.story.js` which is used by Storybook to generate versions of your component based on fixture file:
106* run `npm run spdt:generate-story-index` to generate `.spdt/index.js` file for Storybook
107* add asserts/expectations for the component in fixture file (see examples below)
108* run `npm run spdt:generate-test-index` to generate `.spdt/test-index.generated.js` file.
109* run `npm run spdt:generate-tests` to generate test files for each React component (which has story and fixture files), e.g. `Comment.generated.spdt.js`
110* run Storybook server `npm run spdt:storybook`
111* run generated tests using jest + puppeteer `npm run spdt:test` (in another terminal tab)
112
113## How to install **spdt**
114
115* Install it from npm `npm i -D spdt`
116* Run initialization `node_modules/.bin/spdt:init` It will copy config files (jest, puppeteer, storybook) to predefined folder (by default `./spdt` )
117* Copy generated scripts from terminal to your __package.json__ file
118
119```
120 "spdt:generate-story-index": "./node_modules/.bin/spdt:generate-story-index",
121 "spdt:generate-test-index": "./node_modules/.bin/spdt:generate-test-index",
122 "spdt:generate-tests": "./node_modules/.bin/spdt:generate-tests",
123 "spdt:test": "jest --detectOpenHandles --config ./.spdt/jest.spdt.config.js",
124 "spdt:test:chrome": "HEADLESS=false jest --detectOpenHandles --config ./.spdt/jest.spdt.config.js",
125 "spdt:test:chrome:slow": "SLOWMO=1000 HEADLESS=false jest --detectOpenHandles --config ./.spdt/jest.spdt.config.js",
126 "spdt:storybook": "start-storybook -p 9009 -c ./.spdt",
127 "spdt": "npm run spdt:generate-story-index && npm run spdt:generate-test-index && npm run spdt:generate-tests && npm run spdt:storybook"
128```
129
130## Which npm modules need to be installed
131* @storybook/react@5.x (if you still using storybook v4 please use version spdt@1.1.6)
132* @babel/node@^7.2 (@babel libs are used for generating test files)
133* @babel/core@^7.3
134* @babel/plugin-transform-runtime@^7.3
135* puppeteer@^1.20.0
136* jest-puppeteer@^4.2.0
137* react@16.x
138
139## How to use
140
141For example you have a react component like this
142
143```
144// src/components/SimpleComponent+.js
145
146import React from 'react'
147
148export default function(props) {
149 const { title, children } = props
150 return (
151 <div className="simple-component">
152 {title}
153 <br />
154 {children}
155 </div>
156 )
157}
158```
159
160Create a fixture file `SimpleComponent.fixture.js` inside of `__tests__` folder near the component
161
162```
163// file src/components/__tests__/SimpleComponent.fixture.js
164
165export default {
166 fixtureOne: {
167 props: {
168 title: 'Component Title',
169 children: 'Some children components',
170 },
171 spdt: {
172 checkSelector: 'div.simple-component',
173 },
174 },
175 fixtureTwo: {
176 props: {
177 title: 'Another Title',
178 children: 'Some children components',
179 },
180 spdt: {
181 checkSelector: 'div.simple-component',
182 },
183 },
184}
185```
186
187Here is a schema of the fixture file
188```
189{
190 [unique name of fixture]: {
191 props: { ... }, // list of all props for your component
192 spdt: { ... }, // list of assertions such as checkSelector, checkAxes, checkBars, checkArcs, etc.
193 },
194 [another fixture]: ...
195
196}
197```
198
199Create a file `SimpleComponent.story.js` inside of `__tests__` folder
200
201```
202// file src/components/__tests__/SimpleComponent.story.js
203
204const { storiesOf } = require('@storybook/react')
205import SimpleComponent from '../SimpleComponent'
206import fixtures from './SimpleComponent.fixture'
207
208export default (storyGenerator) =>
209 storyGenerator({
210 storiesOf,
211 title: 'SimpleComponent',
212 Component: SimpleComponent,
213 fixtures,
214 })
215```
216
217Now run `npm run spdt` to call four commands sequencially
218* `spdt:generate-story-index` - it will generate `./.spdt/index.js` for Storybook
219* `spdt:generate-test-index` - it will generate a `./.spdt/test-index.generated.js` for TestGenerator
220* `spdt:generate-tests` - it will generate `<your component name>.generated.spdt.js` files based on pairs (story.js + fixture.js)
221* `spdt:storybook` - it will run Storybook server, available at http://localhost:9009
222
223If everything went well and Storybook started to work you can run generated tests
224```
225npm run spdt:test
226```
227
228## example of a complicated component with dependencies such as Redux, Router
229
230```
231// file src/components/Article/Comment.js
232
233import DeleteButton from './DeleteButton'
234import { Link } from 'react-router-dom'
235import React from 'react'
236
237const Comment = (props) => {
238 const comment = props.comment
239 const show = props.currentUser && props.currentUser.username === comment.author.username
240 return (
241 <div className="card">
242 <div className="card-block">
243 <p className="card-text">{comment.body}</p>
244 </div>
245 <div className="card-footer">
246 <Link to={`/@${comment.author.username}`} className="comment-author">
247 <img src={comment.author.image} className="comment-author-img" alt={comment.author.username} />
248 </Link>
249 &nbsp;
250 <Link to={`/@${comment.author.username}`} className="comment-author">
251 {comment.author.username}
252 </Link>
253 <span className="date-posted">{new Date(comment.createdAt).toDateString()}</span>
254 <DeleteButton show={show} slug={props.slug} commentId={comment.id} />
255 </div>
256 </div>
257 )
258}
259
260export default Comment
261```
262
263A file `Comment.fixture.js` inside of `__tests__` folder
264
265```
266// file src/components/Article/__tests__Comment.fixture.js
267
268export default {
269 fixture1: {
270 props: {
271 comment: {
272 id: 'id123',
273 body: 'some text',
274 author: {
275 username: 'Author name',
276 image: 'https://i.pinimg.com/originals/b6/89/81/b6898148bfa9df9e67330fca31571f9b.png',
277 },
278 createdAt: 'Sat Aug 25 2018',
279 },
280 slug: 'slug123',
281 currentUser: {
282 username: 'user name',
283 },
284 },
285 spdt: {
286 checkSelector: ['div.card', 'div.card-footer', 'img.comment-author-img']
287 },
288 },
289}
290```
291
292
293A file `Comment.story.js` inside of `__tests__` folder
294
295```
296import React from 'react'
297import { Route, BrowserRouter, browserHistory } from 'react-router-dom'
298import { Provider } from 'react-redux'
299import reducer from '../../../reducer'
300import { createStore } from 'redux'
301import Comment from '../Comment'
302import fixtures from './Comment.fixture'
303const { storiesOf } = require('@storybook/react')
304
305const Component = (props) => (
306 <div>
307 <Provider store={createStore(reducer)}>
308 <BrowserRouter history={browserHistory}>
309 <Route path="/" component={() => <Comment {...props} />} />
310 </BrowserRouter>
311 </Provider>
312 </div>
313)
314
315export default (storyGenerator) =>
316 storyGenerator({
317 storiesOf,
318 title: 'Comment',
319 Component: Component,
320 fixtures,
321 })
322```
323
324Run `npm run spdt` and then `npm run spdt:test` in another terminal tab
325and you should see results of jest test runner
326
327```
328> npm run spdt:test
329
330> react-redux-realworld-example-app@0.1.0 spdt:test .../react-redux-realworld-example-app
331> jest --detectOpenHandles --config ./.spdt/jest.spdt.config.js
332
333Setup Test Environment.
334 PASS src/components/Article/__tests__/Comment.generated.spdt.js
335 Comment - fixture fixture1
336 ✓ should find component matching selector [div.card] 1 time(s) (16ms)
337 ✓ should find component matching selector [div.card-footer] 1 time(s) (8ms)
338 ✓ should find component matching selector [img.comment-author-img] 1 time(s) (4ms)
339
340Test Suites: 1 passed, 1 total
341Tests: 3 passed, 3 total
342Snapshots: 0 total
343Time: 1.372s, estimated 2s
344Ran all test suites.
345Teardown Puppeteer
346Teardown Test Environment.
347```
348
349
350## SPDT Declarations
351
352Use these declarations as keys in fixtures
353```
354export default {
355 [fixture name]: {
356 props: { ... },
357 spdt: {
358 [declaration name]: [declaration value]
359 }
360 }
361}
362```
363### checkSelector
364
365Value can be
366* `string` , e.g. 'div.className'
367* `object` , e.g. {selector: 'div.className', length: 0}
368* `array` of strings or objects, e.g. ['div.className', {selector: 'li', length: 5}]
369
370This assertion will generate a separate `it` test to check provided selector
371
372```
373 it('should find component matching selector [div.card] 1 time(s)', async () => {
374 const components = await page.$$('div.card')
375 const expected = 1
376 expect(components).toHaveLength(expected)
377 }),
378```
379
380### checkSvg
381
382Value can be of `Boolean` type
383
384* value `true` means that `it` test will be generated to check `svg` tag is present on the page
385* value `false` means that `it` test won't be generated
386
387```
388 it('should load component as <svg>', async () => {
389 const component = await page.$('svg')
390 expect(component._remoteObject.description).toMatch('svg') // eslint-disable-line no-underscore-dangle
391 })`
392```
393
394### checkAxes
395
396Value can be of `Number` type
397
398* value means the number of expected axes of D3 chart based on selector `g.axis`
399
400```
401 it('should have ${checkAxes} axes', async () => {
402 const axes = await page.$$('g.axis')
403 const expected = ${checkAxes}
404 expect(axes).toHaveLength(expected)
405 })`
406```
407
408### checkBars
409
410Value can be of `Boolean` type
411
412* value `true` means that `it` test will be generated to check selector `rect.bar` has found elements as many as in array `fixture.props.data`
413* value `false` means that `it` test won't be generated
414
415```
416 it('should have ${checkBarsValue} bars according to fixture data', async () => {
417 const bars = await page.$$('rect.bar')
418 const expected = ${checkBarsValue} // fixture.props.data.length
419 expect(bars).toHaveLength(expected)
420 })`
421```
422
423### checkArcs
424
425Value can be of `Boolean` type
426
427* value `true` means that `it` test will be generated to check selector `path.arc` has found elements as many as in array `fixture.props.data`
428* value `false` means that `it` test won't be generated
429
430```
431 it('should have ${checkArcsValue} arcs according to fixture data', async () => {
432 const arcs = await page.$$('path.arc')
433 const expected = ${checkArcsValue} // fixture.props.data.length
434 expect(arcs).toHaveLength(expected)
435 })`
436```
437
438### Custom Declarations
439When you run initialization `node_modules/.bin/spdt:init`
440it creates the file *test-declarations.js* and a directory *custom-declarations* in the folder *.spdt*
441Use the example *testH1* declaration as a guideline to add more custom declarations
442
443General requirements
444* The file *test-declarations.js* should export an object.
445* The key of the object is the name of a custom declaration
446* The value of the object is a function which takes a fixture and returns a string - generated **it** test for puppeteer+jest environment
447
448Example
449```javascript
450const declarationTestH1 = (fixture) => {
451 const { testH1 } = (fixture && fixture.spdt) || {}
452 let selector
453 let value
454 if (typeof testH1 === 'string') {
455 selector = 'h1'
456 value = testH1
457 }
458 if (!selector || !value) {
459 return null
460 }
461 return `
462 it('testH1: should find component matching selector [${selector}] with value ${value}', async () => {
463 const components = await page.$$eval('[id=root] ${selector}', elements => elements.map(e => e.innerText))
464 const expected = '${value}'
465 expect(components).toContain(expected)
466 })`
467}
468
469module.exports = {
470 testH1: declarationTestH1,
471}
472```
473
474## Continuous Integration workflow
475
476* make sure you have installed the module `start-server-and-test`
477
478```
479npm i -D start-server-and-test
480```
481* check `package.json` icludes these script commands
482```
483 "spdt:storybook:ci": "start-storybook --ci --quiet -p 9009 -c ./.spdt",
484 "spdt:ci": "npm run spdt:generate-story-index && npm run spdt:generate-test-index && npm run spdt:generate-tests && npm run spdt:storybook:ci",
485 "ci": "start-server-and-test spdt:ci 9009 spdt:test"
486```
487* just run `npm run ci`
488
489* module `start-server-and-test` does the magic:
490 * it executes `npm run spdt:ci`
491 * it listens when the port 9009 is available (storybook is up and running)
492 * it runs the tests `npm run spdt:test`
493 * it stops storybook when tests end