{"id":230,"date":"2015-12-21T12:25:14","date_gmt":"2015-12-21T03:25:14","guid":{"rendered":"https:\/\/p-corporate-blog-cms.mmmcorp.co.jp\/blog\/2015\/12\/21\/redux-test"},"modified":"2015-12-21T12:25:14","modified_gmt":"2015-12-21T03:25:14","slug":"redux-test","status":"publish","type":"post","link":"https:\/\/p-corporate-blog-cms.mmmcorp.co.jp\/blog\/2015\/12\/21\/redux-test\/","title":{"rendered":"React+Redux\u306e\u30c6\u30b9\u30c8\u65b9\u91dd\u3092\u307e\u3068\u3081\u305f"},"content":{"rendered":"
React.js Advent Calendar<\/a>21\u65e5\u76ee\u306e\u8a18\u4e8b\u3067\u3059\u3002<\/p>\n Redux\u3068\u3044\u3046\u30d5\u30ec\u30fc\u30e0\u30ef\u30fc\u30af\u304c\u3058\u308f\u3058\u308f\u5e83\u307e\u3063\u3066\u3044\u308b\u3002Redux\u306f\u3001Flux\u306e\u6982\u5ff5\u3092\u62e1\u5f35\u3057\u305f\u3082\u306e\u3067\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3067\u3072\u3068\u3064\u306e\u72b6\u614b\u3092\u3082\u3064\u3068\u3001\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3067\u306e\u72b6\u614b\u7ba1\u7406\u304c\u3044\u308d\u3044\u308d\u4fbf\u5229\u306b\u306a\u308b\u3088\u3001\u3068\u3044\u3046\u30b3\u30f3\u30bb\u30d7\u30c8\u3092\u6301\u3064\u3002\u8a73\u7d30\u306f\u4ee5\u4e0b\u306e\u8a18\u4e8b\u304c\u8a73\u3057\u3044\u3002<\/p>\n \u7b46\u8005\u306f\u73fe\u5728React+Redux\u3067\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u3064\u304f\u3063\u3066\u3044\u308b\u304c\u3001\u4eca\u56de\u306f\u3001\u305d\u306e\u30c6\u30b9\u30c8\u65b9\u91dd\u3092\u66f8\u3053\u3046\u3068\u601d\u3046\u3002<\/p>\n karma+jasmine+sinon\u3067\u3064\u304f\u308b\u3002E2E\u306f\u3044\u308d\u3044\u308d\u3068\u30fb\u30fb\u30fb\u306a\u306e\u3067\u30fb\u30fb\u30fb\u30e1\u30a4\u30f3\u306f\u30e6\u30cb\u30c3\u30c8\u30c6\u30b9\u30c8\u3067\u5b9f\u88c5\u3057\u3066\u3044\u308b\u3002JavaScript DOM\u3092\u3064\u304b\u3063\u3066Simulation\u3059\u308c\u3070\u3001\u6700\u4f4e\u9650\u306f\u62c5\u4fdd\u3067\u304d\u308b\u304b\u306a\u3068\u8003\u3048\u308b\u3002<\/p>\n \u5927\u304d\u306a\u65b9\u91dd\u3068\u3057\u3066\u306f\u3001\u5f79\u5272\u3054\u3068\u306b\u5207\u308a\u5206\u3051\u3066\u30c6\u30b9\u30c8\u3092\u66f8\u304f\u3001\u3068\u3044\u3046\u306e\u304c\u3042\u308a\u3001\u305d\u306e\u4e2d\u3067\u3082\u3001view(component), action, reducer\u306b\u7d5e\u3063\u3066\u89e3\u8aac\u3092\u3059\u308b\u3002\u305d\u3046\u3059\u308b\u3068\u4f55\u304c\u3044\u3044\u304b\u3068\u3044\u3046\u3068\u3001\u30c6\u30b9\u30c8\u306e1\u30d5\u30a1\u30a4\u30eb\u306e\u898b\u901a\u3057\u304c\u3068\u3066\u3082\u826f\u304f\u306a\u308a\u3001\u30b7\u30f3\u30d7\u30eb\u306b\u306a\u308b\u3002<\/p>\n \u3072\u3068\u3064\u3072\u3068\u3064\u898b\u3066\u3086\u304f\u3053\u3068\u306b\u3059\u308b\u3002<\/p>\n view(component)\u306e\u30c6\u30b9\u30c8\u304c\u62c5\u5f53\u3059\u308b\u306e\u306f\u3001\u30c7\u30fc\u30bf\u306e\u8868\u793a\u3084\u3001DOM\u30a4\u30d9\u30f3\u30c8\u306b\u3088\u3063\u3066\u304a\u3053\u308b\u5909\u5316\u3067\u3042\u308b\u3002\u5177\u4f53\u7684\u306a\u4f8b\u3092\u307f\u3066\u3086\u304f\u3002<\/p>\n \u4ee5\u4e0b\u306e\u69d8\u306a\u30ab\u30a6\u30f3\u30bf\u30fc\u306e\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u3092\u60f3\u5b9a\u3057\u3001\u30af\u30ea\u30c3\u30af\u3057\u305f\u969b\u306e\u6319\u52d5\u3092\u30c6\u30b9\u30c8\u3059\u308b\u3068\u4eee\u5b9a\u3002<\/p>\n \u3053\u3061\u3089\u306e\u30c6\u30b9\u30c8\u306f\u4ee5\u4e0b\u306e\u3088\u3046\u306b\u306a\u308b\u3002sinon.js\u3067\u95a2\u6570\u3092spy\u3057\u3001\u5b9f\u884c\u3055\u308c\u305f\u304b\u3069\u3046\u304b\u306e\u30c6\u30b9\u30c8\u3092\u3057\u3066\u3044\u308b\u3002<\/p>\n \u975e\u540c\u671f\u306a\u30c7\u30fc\u30bf\u53d6\u5f97\u3084\u3001state\u306e\u5909\u66f4\u306a\u3069\u306f\u3053\u3053\u3067\u306f\u8cac\u52d9\u3068\u3057\u3066\u6301\u305f\u306a\u3044\u3002\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001dispatch\u304c\u304a\u3053\u308b\u304b?\u3060\u3051\u3092\u3053\u3053\u3067\u306f\u30c6\u30b9\u30c8\u3059\u308c\u3070\u3088\u3044\u306e\u3067\u3001\u30b7\u30f3\u30d7\u30eb\u306a\u5b9f\u88c5\u306b\u306a\u308b\u3002\u8a73\u7d30\u306f\u4ee5\u4e0b\u306b\u66f8\u3044\u3066\u304a\u3044\u305f\u306e\u3067\u6c17\u306b\u306a\u308b\u4eba\u306f\u898b\u3066\u307b\u3057\u3044\u3002<\/p>\n \u6b21\u306b\u3001action\u306e\u30c6\u30b9\u30c8\u3092\u5b9f\u88c5\u3059\u308b\u3002action\u306e\u30c6\u30b9\u30c8\u304c\u62c5\u5f53\u3059\u308b\u306e\u306f\u3001\u975e\u540c\u671f\u95a2\u6570\u306a\u3069\u306e\u30e1\u30a4\u30f3\u30ed\u30b8\u30c3\u30af\u3067\u3042\u308b\u3002\u62c5\u5f53\u7bc4\u56f2\u306f\u3001\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\u3057\u3001\u6b63\u3057\u3044action\u304cdispatch\u3055\u308c\u3066\u3044\u308b\u304b<\/strong>\u3067\u3001\u305d\u306e\u5f8c\u306freducer\u30c6\u30b9\u30c8\u306e\u62c5\u5f53\u7bc4\u56f2\u3068\u306a\u308b\u3002<\/p>\n state\u306e\u5909\u66f4\u3092\u30c6\u30b9\u30c8\u3057\u306a\u304f\u3066\u826f\u3044\u306e\u3067\u3001\u30b7\u30f3\u30d7\u30eb\u306b\u306a\u308b\u304c\u3001\u975e\u540c\u671f\u306a\u51e6\u7406\u304c\u5165\u3063\u3066\u304f\u308b\u306e\u3067\u5c11\u3057\u5de5\u592b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308b\u3002\u5b9f\u969b\u306b\u4f8b\u3092\u898b\u3066\u3086\u304f\u3002<\/p>\n \u4ee5\u4e0b\u306e\u69d8\u306aaction\u3092\u60f3\u5b9a\u3059\u308b\u3002<\/p>\n \u3053\u306e\u30c6\u30b9\u30c8\u306f\u3001\u4ee5\u4e0b\u306e\u3088\u3046\u306b\u306a\u308b\u3002 \u5185\u90e8\u3067\u306f\u3001\u60f3\u5b9a\u3055\u308c\u305faction( \u3053\u306e\u3088\u3046\u306b\u3001action\u3092\u4f5c\u6210\u3059\u308b\u307e\u3067\u306e\u30ed\u30b8\u30c3\u30af\u306b\u96c6\u4e2d\u3057\u3066\u30c6\u30b9\u30c8\u3092\u66f8\u304f\u3053\u3068\u304c\u3067\u304d\u308b\u3002\u8a73\u7d30\u306f\u4ee5\u4e0b\u306b\u66f8\u3044\u3066\u3042\u308b\u306e\u3067\u6c17\u306b\u306a\u308b\u4eba\u306f\u898b\u3066\u307b\u3057\u3044\u3002<\/p>\n reducer\u3067\u306f\u3001\u53d7\u3051\u53d6\u3063\u305f\u30c7\u30fc\u30bf\u3092\u52a0\u5de5\u3057\u3001\u6b63\u3057\u3044state\u3092\u8fd4\u3057\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u30c6\u30b9\u30c8\u3059\u308b\u3002\u57fa\u672c\u7684\u306b\u3001reducer\u306f\u305d\u3053\u307e\u3067\u5927\u304d\u306a\u51e6\u7406\u306f\u3057\u306a\u3044\u3068\u601d\u3046\u306e\u3067\u3001\u30c6\u30b9\u30c8\u3082\u30b7\u30f3\u30d7\u30eb\u306b\u306a\u308b\u3002\u516c\u5f0f<\/a>\u306e\u30c6\u30b9\u30c8\u3092\u305d\u306e\u307e\u307e\u306e\u305b\u3066\u307f\u308b\u304c\u3001\u4ee5\u4e0b\u306e\u3088\u3046\u306b\u306a\u308b\u3002<\/p>\n \u4e0a\u306e\u3088\u3046\u306areducer\u306f\u4ee5\u4e0b\u306e\u3088\u3046\u306b\u30c6\u30b9\u30c8\u3067\u304d\u308b\u3002<\/p>\n \u3042\u308b\u7a0b\u5ea6\u30d1\u30bf\u30fc\u30f3\u304c\u6c7a\u307e\u3063\u3066\u304f\u308b\u306e\u3067\u3001\u30c6\u30b9\u30c8\u304c\u66f8\u304d\u3084\u3059\u3044\u3002\u307e\u305f\u3001\u5909\u306b\u3001view, action, reducer\u3092\u4e00\u7dd2\u304f\u305f\u306b\u30c6\u30b9\u30c8\u3059\u308b\u3088\u308a\u3082\u3001\u554f\u984c\u3092\u898b\u3064\u3051\u3084\u3059\u3044\u306e\u304b\u306a\u3068\u3082\u601d\u3063\u305f\u3002<\/p>\n \u307e\u305f\u3001\u30c6\u30b9\u30c8\u65b9\u91dd\u306b\u3064\u3044\u3066\u3060\u3051\u3067\u306a\u304f\u3001\u516c\u5f0f\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8<\/a>\u304c\u3044\u308d\u3044\u308d\u53c2\u8003\u306b\u306a\u308b\u306e\u3067\u898b\u3066\u304a\u304f\u3068\u3044\u3044\u304b\u3082\u3057\u308c\u306a\u3044\u3002<\/p>\n\n
\u30c6\u30b9\u30c8\u74b0\u5883<\/h2>\n
\u30c6\u30b9\u30c8\u65b9\u91dd<\/h2>\n
view<\/h3>\n
function increment() {\n return { type : COUNTER_INCREMENT };\n}\n\nexport class Counter extends React.Component {\n static propTypes = {\n dispatch: React.PropTypes.func.isRequired,\n counter: React.PropTypes.number,\n }\n\n constructor() {\n super();\n }\n\n render() {\n return (\n <div className='container text-center'>\n <h1>Sample Counter<\/h1>\n <h2>{this.props.counter}<\/h2>\n <button onClick={this.props.dispatch(increment)}>\n Increment\n <\/button>\n <\/div>\n );\n }\n}<\/code><\/pre>\n
import React from 'react';\nimport TestUtils from 'react-addons-test-utils';\nimport { Counter } from '.\/path\/to\/counter.js';\n\nfunction renderWithProps (props = {}) {\n \/\/ \u4eee\u60f3DOM\u3092\u63cf\u753b\u3059\u308b\n return TestUtils.renderIntoDocument(<Counter {...props} \/>);\n}\n\ndescribe('Counter', function () {\n let _rendered, _props, _spies;\n\n beforeEach(function () {\n _spies = {};\n\n \/\/ props\u306e\u30e2\u30c3\u30af\n _props = {\n \/\/ sinon.js\u3092\u4f7f\u7528\u3057\u3001\u95a2\u6570\u3092spy\u3059\u308b\n dispatch: _spies.dispatch = sinon.spy(),\n counter: 0,\n };\n\n _rendered = renderWithProps(_props);\n });\n\n \/\/ \u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u30c6\u30b9\u30c8\u306a\u306e\u3067\u95a2\u6570\u304c\u5b9f\u884c\u3055\u308c\u308b\u304b\u3069\u3046\u304b\u307e\u3067\u3092\u30c6\u30b9\u30c8\u3059\u308c\u3070\u3088\u3044\n it('Increment\u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3067dispatch\u304c\u8d70\u308b\u3053\u3068', function () {\n const btn = TestUtils.scryRenderedDOMComponentsWithTag(_rendered, 'button');\n TestUtils.Simulate.click(btn[0]);\n\n \/\/ spy\u3055\u308c\u305f\u95a2\u6570\u304c\u5b9f\u884c\u3055\u308c\u305f\u304b\u3069\u3046\u304b\u3092\u30c1\u30a7\u30c3\u30af\u3059\u308b\n expect(_spies.dispatch.called).toBe(true);\n });\n});<\/code><\/pre>\n
\n
action<\/h3>\n
import {\n REQUEST,\n REQUEST_SUCCESS,\n} from 'constants';\n\n\/\/ \u30ea\u30bd\u30fc\u30b9\u7ba1\u7406\u306fsuperagent\nimport request from 'superagent';\n\nfunction request() {\n return {\n type: REQUEST,\n };\n}\n\nfunction requestSuccess(items) {\n return {\n type: REQUEST_SUCCESS,\n items: items,\n };\n}\n\nexport function fetchSomeResource() {\n return (dispatch, getState) => {\n dispatch(request());\n request\n .get('http:\/\/apiserver.com\/someuri')\n .end(function(err, res) {\n return dispatch(requestSuccess(res.body.items));\n });\n };\n}<\/code><\/pre>\n
mockStore<\/code>\u306f\u3001
redux-mock-store<\/code><\/a>\u3068\u3044\u3046\u30e9\u30a4\u30d6\u30e9\u30ea\u3092\u4f7f\u3063\u3066\u3001store\u5c64\u3092\u30e2\u30c3\u30af\u3057\u3066\u3044\u308b\u3002<\/p>\n
expectedActions<\/code>)\u304cstore\u5c64\u3067dispatch\u3055\u308c\u308b\u304b\u3001\u3068\u3044\u3046\u306e\u3092\u30c6\u30b9\u30c8\u3057\u3066\u304f\u308c\u3066\u3044\u308b\u3002<\/p>\n
import {\n REQUEST,\n REQUEST_SUCCESS,\n} from 'constants';\nimport configureMockStore from 'redux-mock-store';\nimport thunk from 'redux-thunk';\nimport { fetchSomeResource } from '.\/action.js';\n\n\ndescribe('(Action)', function() {\n const middlewares = [ thunk ];\n const mockStore = configureMockStore(middlewares);\n const initialState = {\n items: [],\n isFetching: false,\n };\n\n const expectedActions = [\n { type: REQUEST },\n { type: REQUEST_SUCCESS, items: ['items'] },\n ];\n\n it('REQUEST_SUCCESS\u304cdisapatch\u3055\u308c\u308b\u3053\u3068', (done) => {\n const store = mockStore(initialState, expectedActions, done);\n store.dispatch(fetchSomeResource());\n });\n});<\/code><\/pre>\n
\n
\n
reducer<\/h3>\n
import { ADD_TODO } from '..\/constants\/ActionTypes'\n\nconst initialState = [\n {\n text: 'Use Redux',\n completed: false,\n id: 0\n }\n]\n\nexport default function todos(state = initialState, action) {\n switch (action.type) {\n case ADD_TODO:\n return [\n {\n id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,\n completed: false,\n text: action.text\n },\n ...state\n ]\n\n default:\n return state\n }\n}<\/code><\/pre>\n
import expect from 'expect'\nimport reducer from '..\/..\/reducers\/todos'\nimport * as types from '..\/..\/constants\/ActionTypes'\n\ndescribe('todos reducer', () => {\n it('should handle ADD_TODO', () => {\n expect(\n reducer([], {\n type: types.ADD_TODO,\n text: 'Run the tests'\n })\n ).toEqual(\n [\n {\n text: 'Run the tests',\n completed: false,\n id: 0\n }\n ]\n )\n })\n})<\/code><\/pre>\n
\u61f8\u5ff5\u30fb\u6ce8\u610f\u70b9<\/h2>\n
\n
connect<\/code>\u3055\u308c\u305f\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306f\u3001\u5207\u308a\u51fa\u3057\u3066\u30c6\u30b9\u30c8\u3057\u305f\u307b\u3046\u304c\u3044\u3044\u304b\u3082\u3057\u308c\u306a\u3044\u3002<\/li>\n
callComponentWillReceiveProps<\/code>,
callComponentWillUnmount<\/code>\u306a\u3069\u3092\u3088\u3076\u6642\u306f\u3084\u3084\u5de5\u592b\u304c\u5fc5\u8981\u3002<\/li>\n
\u307e\u3068\u3081<\/h3>\n