Comments 4
Представьте себе простейший компонент, используемый для аутентификации, в котором есть поле для ввода имени пользователя и кнопка отправки данных. В поле вводят имя, нажимают на кнопку, это вызывает выполнение запроса fetch с введёнными данными. Запрос оказывается выполненным, имя передаётся в хранилище Redux и кэшируется в localStorage. В общем-то, всё это тоже не так уж и сложно.
Теперь попытаемся отправить этот компонент на тестовый рендеринг. К сожалению, тест даже не запустится.
Если этот компонент тестировать "одним куском" (и гордо "не используя shallow рендеринг") — тогда конечно, тест не запустится. Более того, без бэкенда он никогда работать не будет. Только вот отношения к юнит-тестам такой тест тоже не имеет.
Но если разбить этот мега-компонент на составные части, то получится вполне разумная картинка (которую можно "нарисовать" через TDD):
- В поле вводим имя, нажимаем на кнопку — это вызывает метод сервиса signIn (мока, разумеется). Все, больше тест ничего не тестирует.
- В сервисе вызываем метод signIn, проверяем, что он вызывает
fetch
или мок http-клиента - Еще в сервисе тестируем, что при успешном ответе от мока
fetch
или http-клиента — он вызывает нужный метод хранилища. - В метода хранилища — смотрим что он вызывает кеширование...
И т.д.
А если для работы теста нужны reduxState, url, localStorage, fetch и т.п. — может стоит задуматься, а не слишком ли много ответственностей у тестируемого компонента?
Но если разбить этот мега-компонент на составные части
Собственно, весь пост у автора как раз про недостатки этого подхода. См. "принцип 2" в статье. И дальше он дает свое определение модуля (юнита) как единый кусок, который планируется переиспользовать.
Примеров, правда, в статье не хватает. Попробую восполнить пробел:
Single Page Application, состоит из нескольких экранов, переключаемых через React Router. C точки зрения Роутера, каждый экран — черный ящик без свойств (props), который можно отрендерить просто как <SomeScreen/>. В этот черный ящик неявно (через контекст) передается redux store. Соответственно, этот экран тестируется именно так, как используется: описываются тестовые фикстуры с состоянием стора, мокаются глобальные xhr/fetch функции. И тестируется этот экран как единое целое.
Другой пример: панель навигации. Эта панель используется на нескольких экранах. При этом она является контейнером (smart component) в терминологии редакса — то есть напрямую подключена к стору. Так что когда экран рендерит эту панель, он не передает в никакие свойства. Соответственно, и тесты на этот компонент написаны в таком же разрезе — задается контекст с состоянием стора и пр. — вместо тестирование нижележащего "тупого" компонента через пропсы.
Можно заметить, что в примерах выше код панели навигация исполнялся дважды: в собственном тесте компонента панели и в тесте компонента экрана, который включает в себя эту панель.
В целом, это реализация "классического" подхода к тестированию (в противовес "мокистскому" подходу, где каждый компонент был бы протестирован независимо с заглушками на все что можно).
Самая жесть начинается когда вы используете reduxForm например. Много мы тут можем натестировать? А главное — есть ли смысл тестировать подобное без редакса, формы, прогона валидации и тд? Мне кажется, такое тестирование ничего не даст. Вот пример компонента:
const onSubmit = async ({email, password}, dispatch, {userLogin, router}) => {
try {
await userLogin(email, password);
router.push('/');
} catch (e) {
throw new SubmissionError({_error: 'Incorrect email or password.'});
}
};
const validate = (values) => requredFields(values, ['email', 'password']);
let Login = ({handleSubmit, pristine, submitting, error, isAuthorized, location}) => {
if (isAuthorized) {
const {from} = location.state || {from: {pathname: '/'}};
return (<Redirect to={from}/>);
}
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<Field name="email" component={ReduxInput} label="Email" />
<Field name="password" component={ReduxInput} label="Password" type='password'/>
{error && (
<Error>{error}</Error>
)}
<SubmitButton disabled={pristine || submitting}>Login</SubmitButton>
</Form>
);
};
Login = reduxForm({form: Login.name, validate})(Login);
Login = connect((state) => ({
isAuthorized: getToken(state)
}), {userLogin}, ownPropsWin)(Login);
Login = withRouter(Login);
export default Login;
При тесте мы можем подменить экшн, например, connect
это позволяет, чтобы хотя бы по сети не ходить. Но все остальное надо брать и тестировать без разбора на атомарные кусочки, иначе кусочки то 100% будут работать, а самый главный связующий слой так и останется непокрытым.
Тестирование компонентов React