Reduxコンポーネントをユニットテストする方法

この記事は[Unit Testing Redux Connected Components](https://hackernoon.com/unit-testing-redux-connected-components-692fa3c4441c)の一部を和訳したものです。何か問題があったら削除します。

良ければツイッターフォローお願いします。@wabisuke2718

#和訳
この例ではJestとEnzymeを使ってテストします。
Reduxコンポーネントをstoreのモックと関数のモックを使ってテストする方法を説明します。

##どのようにテストするのか
次のようなReduxコンポーネントをテストします。

```js:app/src/components/Login.js
import React from 'react'
import { connect } from 'react-redux'
import { loginUser } from '../actions/users'

class Login extends React.Component {
	constructor() {
		super()
		this.state = {
			username: '',
			password: ''
		}
	}
	
	handleInputChange = (event) => {
		this.setState({
			[event.target.name]: event.target.value
		})
	}
	
	handleSubmit = (event) => {
		event.preventDefault()
		this.props.login(this.state)
	}
	
	render() {
		return (
			<form id='loginForm' className='login' onSubmit={this.handleSubmit}>
				<label>Username</label>
				<input id='email' onChange={this.handleInputChange} name='email' type='text' />
				<label>Password</label>
				<input id='password' onChange={this.handleInputChange} name='password' type='password' />
				<button>Submit</button>
			</form>
		)
	}
}

function mapDispatchToProps(dispatch) {
	return {
		login: (userparams, history) => {
			dispatch(loginUser(userparams, history))
		}
	}
}

export default connect(mapDispatchToProps)(Login)
```

このコンポーネントはRedux storeに関連付けられており、フォームのonSubmitイベントはhandleSubmit関数を呼び出します。
login関数はアクションクリエーターを呼び出します。
フォームが送信されるとlogin関数が呼び出されます。

このReduxコンポーネントをテストしようとすると、次のようなエラーに遭遇するでしょう。

```
Invariant Violation: Could not find "store" in either the context or props of "Connect(Login)". 
Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(Login)".
```

このエラーはJestがReduxのstoreを探し出せなかったことを示しています。
Jestのテストはアプリケーションから独立しており、storeは作成されないし、Reduxのセットアップもされないので、これは当然のことです。

##この問題をどう修正するか
方法はいくつかありますが、主なものは２つです。
コンポーネントをReduxに接続してテストをするか、接続せずにテストをする方法があります。

##storeのモックを使ってテストする方法
この方法は個人的におすすめしません。
この方法を使う場合ユニットテストの範疇を超えてインテグレーションテストになってしまうからです。
前述のエラーメッセージから問題を解決するには２つの方法があります。
明示的にstoreを作成しコンポーネントに渡すか、storeを保持したRedux Providerでコンポーネントをラップするかです。
この中では前者をおすすめします。なぜならmountする必要がなくなり代わりにshallowが使えるからです。
storeを統合する方法の一つはredux-mock-storeを使う方法です。
次のコマンドを実行します。

```
npm install redux-mock-store --save-dev
```

storeのモックの作り方は以下のようになります。

```js:app/src/components/__tests__/Login-test.js
import configureStore from 'redux-mock-store'
 
// create any initial state needed
const initialState = {}; 
// here it is possible to pass in any middleware if needed into //configureStore
const mockStore = configureStore();
let wrapper;
let store;
beforeEach(() => {
	//creates the store with any initial state or middleware needed  
	store = mockStore(initialState)
	wrapper = 次の説明を参照
})
```

そして作成したstoreをコンポーネントに渡す方法は次の２通りです。

```js
wrapper = shallow(<Login store={store}/>)
                        OR
// おすすめしません
wrapper = mount(<Provider store={store}<Login /></Provider>)
```

redux-mock-storeのドキュメントを見ると、このライブラリはアクションクリエーターやミドルウェアのテストのために作られているようで、今回はこの目的で使用していないためあまり良くない可能性があります。
このstoreのモックを使うと、アクションクリエーターを通してstoreに送られたアクションをテストすることができますが、これはインテグレーションテストの範疇になります。

##Redux storeなしでコンポーネントをテストする
Redux storeなしでテストする場合、storeのモックを作成するような無駄な作業は必要なくなります。
これを行うには単にconnectしていないコンポーネントをdefault exportしているconnectしたコンポーネントとともにexportするだけです。
次のようにexportを追加します。

```js:app/src/components/Login.js
import React from 'react'
import { connect } from 'react-redux'

// ↓ここにexportを追加
export class Login extends React.Component {
	// ...code above
}

function mapDispatchToProps(dispatch) {
	return {
		login: (userparams, history) => {
			dispatch(loginUser(userparams, history))
		}
	}
}

export default connect(mapDispatchToProps)(Login)
```

そしてテストコードに以下の行を追加します。

```js:app/src/components/__tests__/Login-test.js
import { Login } from '../Login'
```

これによりconnetされたデフォルトのコンポーネントではなく、connectされていないコンポーネントがインポートされます。
storeが無いのでmapDispatchToProps関数のログイン機能はコンポーネントに提供されません。
しかしJestで関数のモックを作ることで簡単に対処できます。
このモック関数を使うことで関数が呼ばれたかどうかも判定できます。
これを行うには次のコードを追加し、モック関数をlogin propに渡します。

```js:app/src/components/__tests__/Login-test.js
describe('Login Component', () => {
	let wrapper;
	// our mock login function to replace the one provided by mapDispatchToProps
	const mockLoginfn = jest.fn();

	beforeEach(() => {
	// pass the mock function as the login prop 
		wrapper = shallow(<Login login={mockLoginfn}/>)
	})
	// ...tests here...
}
```

これでmapDispatchToPropsを置き換えることができました。
これを用いたテストコードは次のようになります。

```js:app/src/components/__tests__/Login-test.js\
describe('When the form is submitted', () => {
	it('should call the mock login function', () => {
		wrapper.find('#loginForm').simulate(
			'submit', 
			{preventDefault() {}}
		)
		expect(mockLoginfn.mock.calls.length).toBe(1)
	})
})
```

ここで力尽きた。ほとんど和訳したので残りは原文のページを参照のこと