μλ νμΈμ, μ΄κ²μ λ¬Έμ λΌκΈ°λ³΄λ€λ μ§λ¬Έμ κ°κΉμ΅λλ€.
μ΅κ·Όμ νλ‘μ νΈμ Reactμ Reduxλ₯Ό μ¬μ©νκΈ° μμνκ³ ν μ€νΈμ λͺ κ°μ§ λ¬Έμ κ° μμ§λ§ λ¬Έμκ° λ¬Έμ μ λν κ²°μ μ μΈ ν΄κ²°μ± μ μ 곡νμ§ μλ κ² κ°μ΅λλ€.
νμ¬ λλ μΌλΆ λ¨μ ν μ€νΈλ₯Ό μμ±νκ³ μΆμ μ°κ²°λ κ΅¬μ± μμκ° μμ§λ§ 첫 λ²μ§Έ κ΅¬μ± μμ λ΄μ μ€μ²© λ μ°κ²°λ κ΅¬μ± μμκ° μμΌλ©° κ²°κ΅ μν λλ μν (λλ λλΆλΆμ κ²½μ° λ λ€)μ μν΄ μ°¨λ¨λλ κ² κ°μ΅λλ€. μ€μ²© λ κ΅¬μ± μμκ° μ€μ λμ§ μμμ΅λλ€.
ν μ€νΈνλ €λ κ΅¬μ± μμμλ§ μ§μ€ν μ μλλ‘ νλμ κ΅¬μ± μμλ₯Ό μ€ν° λΉνλ λ°©λ²μ΄ μλμ§ κΆκΈν©λλ€.
미리 κ°μ¬λ립λλ€
λκ°μ§:
1) μ΄κ²μ μ¬μ©λ²μ΄λ©° μλ§λ μ€ν μ€λ²νλ‘λ‘ μ΄λν΄μΌν©λλ€.
2) connect
μμ κ΅¬μ± μμ μ¬μ© νΈμμ μΌλΆ
unconnected
κ΅¬μ± μμμ λν λ¨μ ν
μ€νΈλ₯Ό μμ±νκ³
reduxλ₯Ό μ λ’°νμ¬ λ§€μ₯ μ£Όλ³μ λ°°κ΄μ μ²λ¦¬ν΄μΌν©λλ€.
λ΄κ° μλ―Ένλ λ°κ° λͺ ννμ§ μμΌλ©΄ μμ λ₯Ό κ²μ ν μ μμ΅λλ€.
λͺ κ°μ§ μκ° :
<Provider store={testStore}><ConnectedComponent /></Provider>
μ λ λλ§ ν μλ μμ΅λλ€.μ°Έκ³ λ‘ μ¬κΈ° React / Redux ν μ€νΈμ λν μ¬λ¬ κΈ°μ¬μ λν λ§ν¬κ° μμ΅λλ€. λμμ΄ λ μ μμ΅λλ€ :
κ·Έλ¦¬κ³ μ, μ¬μ©λ² μ§λ¬ΈμΌλ‘μ μ΄κ²μ λ€λ₯Έ κ³³μμ μ€μ λ‘ λ μ μ§λ¬Έλ©λλ€.
κ°μ¬ν©λλ€ @markerikson μ λ νμ¬ μΈκΈ ν νμμ λ°©λ²μ μλνκ³ μμ§λ§ λ§ν¬ λͺ©λ‘λ λΆλ§ν¬ ν κ²μ λλ€. :)
ν λ² κ΅¬μ± μμλ₯Ό λΆλ¦¬νμ¬ ν μ€νΈνλ €λ©΄ μ¬μ ν λ€μκ³Ό κ°μ μμ μ μννλ κ²μ΄ μ’μ΅λλ€.
// Component.js
import React from 'react'
export const Component = ({ a, b, c }) => (
// ...
)
// ...
export default connect(mapStateToProps, mapDispatchToProps)(Component)
κ·Έλ° λ€μ ν μ€νΈμμ μ°κ²°λκΈ° μ μ κ΅¬μ± μμλ₯Ό κ°μ Έμ¬ μ μμ΅λλ€.
// Component.spec.js
import { Component } from './Component.js'
import { mount } from 'enzyme'
it('should mount without exploding', () => {
mount(<Component a={1} b={2} c={3} />)
})
μ λ§λ‘ ν
μ€νΈνκ³ μΆμ κ²μ΄ Provider
λ₯Ό λ λλ§νλ κ²μ΄ ν©λ¦¬μ μ΄λΌκ³ μκ°νλ κ²λ³΄λ€ μμ μ λ³κ²½ μ¬νμ΄ μ¬λ°λ₯΄κ² μ νλκ³ μλ€λ κ²μ΄λ©°, ν
μ€νΈμμ λ§μ κ°μΉλ₯Ό μ»μλμ§ νμ€νμ§ μμ΅λλ€.
μ¬κΈ°μ μ κΈ°λλ λ¬Έμ λ ν
μ€νΈμ€μΈ κ΅¬μ± μμκ° μ§μ νμ λλ κ³μΈ΅ μλ μ΄λκ°μ λ€λ₯Έ μ°κ²°λ κ΅¬μ± μμλ₯Ό λ λλ§ ν λμ
λλ€. mount()
λ₯Ό ν΅ν΄ μ 체 λ λλ§μ μννλ©΄ νμ μ°κ²° κ΅¬μ± μμκ° μ λμ μΌλ‘ this.context.store
μ‘μΈμ€λ₯Ό μλνμ§λ§ μ°Ύμ§ λͺ»ν©λλ€. λ°λΌμ μ£Όμ μ΅μ
μ μμ ν
μ€νΈλ₯Ό μννμ¬ νμ νλͺ©μ΄ λ λλ§λμ§ μλλ‘νκ±°λ μ€μ λ‘ μ»¨ν
μ€νΈμμ μ μ₯μλ₯Ό μ¬μ©ν μ μλλ‘ λ§λλ κ²μ
λλ€.
@markerikson μ μ’μ, λλ OPκ° μΈλΆκ° μλ λ΄λΆ κ΅¬μ± μμλ₯Ό ν μ€νΈνλ €κ³ νλ€κ³ μκ°νλ€λ κ²μ λ€μ μ½μμ΅λλ€. π
μ§κΈκΉμ§ λμ μ£Όμ μ κ°μ¬ν©λλ€.
νμ¬ λ΄κ° κ²ͺκ³ μλ μ£Όμ λ¬Έμ λ λ΄λΆμ μ€μ²© λ μ°κ²°λ κ΅¬μ± μμμ μνλ₯Ό μ λ¬ν μ μμ΄ λ·°κ° μ¬λ°λ₯΄κ² λ λλ§λμ§ μλλ€λ κ²μ λλ€.
μΌλ°μ μΌλ‘ μ‘°λ‘± μνλ μ κ° μ€μ λ‘ κ½€ κ³ μ¬ ν΄ μμΌλ©° μ£Όμ μ λν΄ κ²°μ μ μΈ κ²μ μ°Ύμ μ μμκΈ° λλ¬Έμ λκ΅°κ° μ‘°λ‘± μνλ₯Ό μ€μ²© λ μ°κ²° λλ μμ κ΅¬μ± μμ?
TIA
@StlthyLee : "ν΅κ³Ό μν"λ 무μμ μλ―Έν©λκΉ? ν
μ€νΈμμ <Provider store={store}><ComponentWithConnectedDescendants /></Provider>
λ₯Ό λ λλ§νλ©΄ μμ
μ΄ μ²λ¦¬λ©λλ€.
λ¬Έμ λ μ¬λ°λ₯΄κ² μ²λ¦¬νμ§ μλ κ²μ λλ€. κ·Έ μμ μ€μ²© λ κ΅¬μ± μμμλ νμ¬ μ΄λ€ μ΄μ λ‘ μΈν΄ μ»μ§ λͺ»νλ μμ© νλ‘κ·Έλ¨ μνμ νΉμ λ§€κ° λ³μκ° νμν©λλ€.
@StlthyLeeλ μλ§λ μμ / μμ / codepen / repo-link λλ 무μΈκ°λ₯Ό λ£μ΄μΌ ν κ²μ λλ€. μ¬κΈ°μ μ©μ΄ μΆ©λμ΄μλ κ²μ²λΌ 보μ΄κΈ° λλ¬Έμ λ¬Έμ λ₯Ό λ°κ²¬νλ λ° μκ°μ΄ λ 걸립λλ€.
@Koleok ν¬λ§μ μΌλ‘ μ΄κ²μ λͺ ννκ² λμμ΄ λ κ²μ λλ€
https://gist.github.com/StlthyLee/02e116d945867a77c382fa0105af1bf3
κ·Έλ μ§ μλ€λ©΄ λ¬Όμ΄λ³΄μμμ€. μ΄κ²μ΄ μ κ° λ°λ°λ₯μ λλ¬νκ³ μΆμ κ²μ λλ€. μ§λ 9-10 λ λμ Rails μΈκ³μμ μΌνκ³ κ·Έ κ΄μ μμ TDDμ λν΄ μ μ΄ν΄νκ³ μμκΈ° λλ¬Έμ μ λ μ΄μ React / Redux μΈκ³μμ TDDμ λν λ΄ λ¨Έλ¦¬.
κ·Έλμ μ λ μ§κΈμ΄ λ¬Έμ μ λ°λ°λ₯μ λλ¬νλ€κ³ λ―Ώμ΅λλ€. κ·Έκ²μ ν μ€νΈ λ¬Έμ κ° μλλΌ λ€λ₯Έ νμμ΄ λ§λ μΉ΄νΌμ κ³Όκ±°μ μ€λ₯μμ΅λλ€. μ νΈνλ TDD λ°©μ보λ€λ μ½λμ λ§κ² ν μ€νΈν©λλ€. μ¬κΈ°μ μ 곡λ λͺ¨λ μ 보μ κ°μ¬λ립λλ€.
μΌλΆ μ¬μ© μ¬λ‘μμ μλ ν μμλ λ λ€λ₯Έ μ΅μ μ μ 곡νκΈ° μν΄ μ΅κ·Όμ μ€μ²© λ μ°κ²° κ΅¬μ± μμμμμ΄ λ¬Έμ κ° λ°μνμ΅λλ€. λͺ¨μ μ μ₯μλ₯Ό λ§λλ λμ κ° κ΅¬μ± μμμ μ°κ²°λμ§ μμ λ²μ μ λ΄λ³΄λ΄ μ μ₯μμμ΄ λ¨μ ν μ€νΈλ₯Ό μννμ΅λλ€. μ΄ κ°μ:
class TopLevelImpl extends React.PureComponent {
// Implementation goes here
}
// Export here for unit testing. The unit test imports privates and uses TopLevel
// with Enzyme's mount().
export const privates = {
TopLevel: TopLevelImpl,
}
export const TopLevel = connect(mapStateToProps)(TopLevelImpl)
μμ μ€μ²© κ΅¬μ± μμμ λν΄ λμΌν μμ μ μννμ¬ κ° κ΅¬μ± μμλ₯Ό λ 립μ μΌλ‘ ν μ€νΈ ν μ μμ΅λλ€.
κ·Έλ° λ€μ μ΅μμ μ°κ²° κ΅¬μ± μμλ₯Ό κ°μ Έ μμ μ±μ render()
ν¨μμμ μνμΌλ‘ μμ μ°κ²° κ΅¬μ± μμλ₯Ό μ λ¬νμ΅λλ€. κ·Έλ¬λ μ΅μμ κ΅¬μ± μμλ₯Ό λ¨μ ν
μ€νΈ ν λ μ°κ²°λ κ΅¬μ± μμλ₯Ό μ λ¬νλ λμ λͺ¨μ κ΅¬μ± μμλ₯Ό μ λ¬νμ¬ μ¬λ°λ₯Έ μμΉμ λ λλ§λλμ§ νμΈν©λλ€. κ·Έλ¬λ μ°κ²°λμ§ μμ κ΅¬μ± μμλ₯Ό μ λ¬ν μλ μμ΅λλ€.
μ΄ κ°μ:
// Root render function
render() {
return (
<Provider store={store}>
<TopLevel child1={<Child1 />} child2={<Child2 />} />
</Provider>
)
}
λλ μ€μ λ‘ λΉμ μ μμ΄λμ΄λ₯Ό μ’μν©λλ€. νμ§λ§ λμ μ΄λ κ² ν΄μ
const props = {
child1: () => (<div />),
child2: () => (<div />)
}
<TopLevel {...props} />
μ΄λ―Έ child1κ³Ό child2λ₯Ό ν μ€νΈνλ€λ©΄ TopLevel κ΅¬μ± μμμ ν¬ν¨ ν νμκ° μμ κ²μ λλ€.
κ΅¬μ± μμλ₯Ό 쑰건λΆλ‘ λ λλ§ν΄μΌνλ κ²½μ°μλ ν¨μλ₯Ό μ λ¬ν©λλ€.
λλ λΉμ μ λ§μ§λ§ λ¬Έμ₯μ λ°λ₯΄μ§ μμ΅λλ€. ν μ€νΈλ νμ κ΅¬μ± μμλ₯Ό λ€λ₯Έ κ΅¬μ± μμλ‘ μ λ¬νλ κ²κ³Ό μ΄λ€ κ΄λ ¨μ΄ μμ΅λκΉ?
μ. μλ₯Ό λ€μ΄ νλ λλ λͺ κ°μ λ΄λΆ μ°κ²° κ΅¬μ± μμ λ₯Ό μ¬μ©νλ μμκ° μμ΅λλ€. μ΄ κ²½μ° μΈλΆ κ΅¬μ± μμλ₯Ό ν μ€νΈνλ €λ©΄ λ€μκ³Ό κ°μ΄ μνν©λλ€.
wrapper = mount(<ExternalComponent.WrappedComponent {...props} />)
κ·Έλ¬λ λ΄λΆ κ΅¬μ± μμ κ° μ λλ‘ μ€ν νμνλ€λ μ€λ₯ κ° μμ΅λλ€.
Invariant Violation: Could not find "store" in either the context or props
κ·Έλ° λ€μ λͺ¨μ μν λ±μ μ λ¬νλ €κ³ νμ΅λλ€. κ·Έλ¬λ μ κ²½μ°μλ λΉλκΈ° νΈμΆ, initialState λ±μ ν¬ν¨νμ¬ μ°κ²°λ λͺ¨λ κ΅¬μ± μμμ λν΄ λ§μ μ΄κΈ°νλ₯Ό μνν΄μΌνλ©° μ΄μ κ΄λ ¨λ λ€λ₯Έ μ€λ₯κ° λ°μνμ΅λλ€.
μΈλΆ κ΅¬μ± μμλ₯Ό ν μ€νΈνκ³ μΆλ€λ©΄ λͺ¨λ λ΄λΆ μ°κ²°λ κ΅¬μ± μμμ λν΄ λͺ¨λ μ μ ν μ΄κΈ°νλ₯Ό μννλ κ²μ΄ κ³Όμμ΄λΌκ³ μκ°ν©λλ€.
κ·Έλμ μ λ μ°κ²°λ λͺ¨λ μ»΄ν¬λνΈ λ₯Ό μΈλΆλ‘ λΉ div λ‘
μ€ μ, κ·Έκ²μ ν¨ν΄μ λν μλμ λκΈ°μμ΅λλ€. λ°λΌμ λ¬Έμ₯μ μμ νλ €λ©΄ "μ΄λ―Έ child1 λ° child2λ₯Ό ν μ€νΈνλ€λ©΄ λ¨μ ν μ€νΈμ TopLevel κ΅¬μ± μμμ μ λ¬ν νμκ° μλ€κ³ μκ°ν©λλ€."λΌλ μλ―Έμ λλ€.
λμν©λλ€.
@StlthyLee , λΆννλ μ°κ²°λ κ΅¬μ± μμκ° μ€μ²© λ κ΅¬μ± μμλ₯Ό ν μ€νΈνλ €κ³ ν λ λμΌν λ¬Έμ μ μ§λ©΄νμ΅λλ€.
νΉν μ μ©ν ν΄κ²°μ± μ ν μ€νΈ νκ²½μ λ°λΌ μ€μ²© λ κ΅¬μ± μμλ₯Ό redux μ μ₯μμ μ°κ²°νλ κ²μ λλ€.
NODE_ENV="test"
ν©λλ€. μ΄ λ³μλ₯Ό ν¬μ°©νκ³ κ΅¬μ± μμκ° κΈ°λ³Έ μμ±μ λ°λλ‘ ν μ μμ΅λλ€. μ΄ κ²½μ° κ΅¬μ± μμκ° μ μ₯μμ μ°κ²°λμ΄ μμ§ μμΌλ©° μμ κ΅¬μ± μμλ₯Ό ν
μ€νΈ ν λ λ¬Έμ κ° λνλμ§ μμ΅λλ€.a) λ¨Όμ νκ²½μ΄ test
μΈμ§ νμΈνλ λμ°λ―Έ ν¨μκ° νμν©λλ€.
export default function ifTestEnv(forTestEnv, forNonTestEnv) {
const isTestEnv = process && process.env && process.env.NODE_ENV === 'test';
const hoc = isTestEnv ? forTestEnv : forNonTestEnv;
return function(component) {
return hoc(component);
};
}
b) λ€μμΌλ‘ defaultProps()
λ° connect()
λ νκ²½μ λ°λΌ 쑰건λΆλ‘ μ μ©λμ΄μΌν©λλ€.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import defaultProps from 'recompose/defaultProps';
import ifTestEnv from 'helpers/if_test_env';
import { fetch } from './actions';
class MyComponent extends Component {
render() {
const { propA, propB } = this.props;
return <div>{propA} {probB}</div>
}
componentDidMount() {
this.props.fetch();
}
}
function mapStateToProps(state) {
return {
propA: state.valueA,
propB: state.valueB
};
}
export default ifTestEnv(
defaultProps({
propA: 'defaultA',
propB: 'defaultB',
fetch() { }
}),
connect(mapStateToProps, { fetch })
)(MyComponent);
c) ν
μ€νΈλ₯Ό μ€νν λ NODE_ENV
μ΄ "test"
νμΈν©λλ€.
NODE_ENV='test' mocha app/**/*.test.jsx
μ λμ¬λ₯Ό μ
λ ₯ν©λλ€.plugins: [
// plugins...
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('test')
},
})
]
@markerikson λ κ°μ§ μ μμ κ°μ¬λ립λλ€. λ λ²μ§Έλ₯Ό μλνκ³ Providerλ‘ ν
μ€νΈνκΈ° μν΄ κ΅¬μ± μμλ₯Ό λννμ΅λλ€. μ΄ κ²½μ° mapStateToProps
λ μ λλ‘ μλνμ§λ§ mapDispatchToProps
μμλλ κ²°κ³Όλ₯Ό μ»μ§ λͺ»ν©λλ€. μ¬κΈ°μ ν
μ€νΈμ€μΈ κ΅¬μ± μμλ λ€λ₯Έ μ°κ²°λ κ΅¬μ± μμλ₯Ό λ λλ§ν©λλ€.
λ΄ mapDispatchToPropsλ λ€μκ³Ό κ°μ΅λλ€.
/* <strong i="10">@flow</strong> */
import { bindActionCreators } from 'redux';
import type { Dispatch } from './types';
import * as actions from './actions';
export default (dispatch: Dispatch, ownProps: Object): Object => ({
actions: bindActionCreators(actions, dispatch),
});
μ¬κΈ°μ λ°ν λ μμ
μ κ°μ undefined
μ
λλ€.
store.js
import { applyMiddleware, compose, createStore } from 'redux';
import { autoRehydrate } from 'redux-persist';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
import { REHYDRATE } from 'redux-persist/constants';
const store = compose(autoRehydrate(), applyMiddleware(thunk, createActionBuffer(REHYDRATE)))(createStore)(rootReducer);
export default store;
λλ μ΄κ²μ΄ κ½€ μ€λλ μ€λ λλΌλ κ²μ μκ³ μμ§λ§ μ΄κ²μ΄ μ½κ° μ±κ°μ λ¬Έμ λΌλ κ²μ μμκ³ μ΄ μ루μ μ΄ (λμ κ°μ)μ΄ μ€λ λλ₯Ό μ°μ°ν λ°κ²¬νλ μ¬λμκ² λμμ΄ λ κ²μ΄λΌκ³ μκ°νμ΅λλ€.
μ€μ λ‘ Reduxμ κ΄λ ¨λ κ²μ ν μ€νΈνμ§ μμκΈ° λλ¬Έμ λ΄κ° μ°Ύμ κ°λ¨ν ν΄κ²°μ± μ μ λ¬ λ λ³κ²½λμ§ μμ κ΅¬μ± μμ λ§ λ°ννλλ‘ μ°κ²° ν¨μλ₯Ό μ‘°λ‘±νλ κ²μ λλ€.
μλ₯Ό λ€μ΄ μμ© νλ‘κ·Έλ¨μ λ€μκ³Ό κ°μ κ΅¬μ± μμκ°μμ μ μμ΅λλ€.
export class ExampleApp extends Component {
render() {
}
}
export default connect(
null,
{ someAction }
)(ExampleApp);
κ·Έλ° λ€μ ν μ€νΈ νμΌμ μμ λΆλΆμ λͺ¨μ μ°κ²° ν¨μλ₯Ό λ°°μΉνμ¬ Reduxκ° κ΅¬μ± μμμ μνΈ μμ©νλ κ²μ μ€μ§ ν μ μμ΅λλ€.
jest.mock("react-redux", () => {
return {
connect: (mapStateToProps, mapDispatchToProps) => (
ReactComponent
) => ReactComponent
};
});
μ΄ μ‘°λ‘±μ μ°κ²°λ μμμ λ§μ΄νΈνλ λͺ¨λ ν μ€νΈ νμΌμ λ°°μΉν΄μΌν©λλ€.
@scottbanyard μ΄λ»κ² μνμ μμ κ΅¬μ± μμμ μ λ¬ν©λκΉ?
κ°μ₯ μ μ©ν λκΈ
μ¬κΈ°μ μ κΈ°λλ λ¬Έμ λ ν μ€νΈμ€μΈ κ΅¬μ± μμκ° μ§μ νμ λλ κ³μΈ΅ μλ μ΄λκ°μ λ€λ₯Έ μ°κ²°λ κ΅¬μ± μμλ₯Ό λ λλ§ ν λμ λλ€.
mount()
λ₯Ό ν΅ν΄ μ 체 λ λλ§μ μννλ©΄ νμ μ°κ²° κ΅¬μ± μμκ° μ λμ μΌλ‘this.context.store
μ‘μΈμ€λ₯Ό μλνμ§λ§ μ°Ύμ§ λͺ»ν©λλ€. λ°λΌμ μ£Όμ μ΅μ μ μμ ν μ€νΈλ₯Ό μννμ¬ νμ νλͺ©μ΄ λ λλ§λμ§ μλλ‘νκ±°λ μ€μ λ‘ μ»¨ν μ€νΈμμ μ μ₯μλ₯Ό μ¬μ©ν μ μλλ‘ λ§λλ κ²μ λλ€.