Pull to refresh

Comments 4

Представьте себе простейший компонент, используемый для аутентификации, в котором есть поле для ввода имени пользователя и кнопка отправки данных. В поле вводят имя, нажимают на кнопку, это вызывает выполнение запроса fetch с введёнными данными. Запрос оказывается выполненным, имя передаётся в хранилище Redux и кэшируется в localStorage. В общем-то, всё это тоже не так уж и сложно.
Теперь попытаемся отправить этот компонент на тестовый рендеринг. К сожалению, тест даже не запустится.

Если этот компонент тестировать "одним куском" (и гордо "не используя shallow рендеринг") — тогда конечно, тест не запустится. Более того, без бэкенда он никогда работать не будет. Только вот отношения к юнит-тестам такой тест тоже не имеет.


Но если разбить этот мега-компонент на составные части, то получится вполне разумная картинка (которую можно "нарисовать" через TDD):


  1. В поле вводим имя, нажимаем на кнопку — это вызывает метод сервиса signIn (мока, разумеется). Все, больше тест ничего не тестирует.
  2. В сервисе вызываем метод signIn, проверяем, что он вызывает fetch или мок http-клиента
  3. Еще в сервисе тестируем, что при успешном ответе от мока fetch или http-клиента — он вызывает нужный метод хранилища.
  4. В метода хранилища — смотрим что он вызывает кеширование...

И т.д.


А если для работы теста нужны reduxState, url, localStorage, fetch и т.п. — может стоит задуматься, а не слишком ли много ответственностей у тестируемого компонента?

Так может это и есть его ответственность: предоставить полное состояние приложения основываясь на 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% будут работать, а самый главный связующий слой так и останется непокрытым.

Sign up to leave a comment.