Вы когда-нибудь писали тест? Разве тестирование не самое худшее? Почему это так ужасно? Почему кажется невозможным написать тесты, которые не страшно поддерживать? Почему люди ненавидят писать тесты? Причин довольно много, но сейчас я просто сосредоточусь на ужасах асинхронного тестирования. Это потому, что у меня всегда возникает соблазн написать очень интерактивные тесты для любой логики пользовательского интерфейса, которую я пишу. Я просто хочу проверить все взаимодействия сразу и убедиться, что они работают, хорошо? Меня тошнит от того, что все постоянно ломается! Но это никогда не бывает так просто. Эти полуинтеграционные полумодульные тесты никогда не работают. Они всегда ломаются, или моя среда тестирования просто не работает так, как я хочу, и я не могу понять, почему. Я обычно сдаюсь.

Подключение асинхронных тестов раздражает. Это раздражает, потому что никакая тестовая среда, кажется, никогда не работала хорошо с этим стилем тестирования. Асинхронные тесты, которые вы пишете, всегда будут ненадежными. Когда в вашем тестовом фреймворке выйдут новые версии, вы не сможете их использовать, потому что они приведут к провалу всех ваших асинхронных тестов. Та тонкая настройка, которую вы сделали год назад, будет напрасной. Вам придется либо 1. поместить эти тесты в карантин и запустить их отдельно со своими собственными зависимостями, 2. удалить их, 3. потратить месяц на их переписывание. Вывод: ни у кого нет на это времени. Мы все занятые люди.

Раньше я ненавидел тестирование. Почему я ненавидел это? И снова асинхронное тестирование. Я хотел протестировать каждое состояние моего пользовательского интерфейса, что означало тестирование взаимодействий. В конечном счете, тестирование взаимодействий невозможно. Фермент 2 относительно хорошо тестировал взаимодействия. Но у фермента 3 есть проблема с React, где вам иногда приходится сообщать обёртке об обновлении. Почему это? Фермент больше не поддерживается? Может ли кто-нибудь дать мне объяснение этому? Серьезно, пожалуйста, прокомментируйте свое отношение к этой проблеме и ссылки на подтверждающие доказательства. Я буду глубоко признателен за это.

В любом случае, да, раньше я ненавидел тестирование. Я потерял всякую надежду на то, что каждый найдет идеальное сочетание инструментов тестирования и соглашения для поддержки тестирования асинхронных взаимодействий, которые я так сильно хочу и должен проверить, чтобы поддерживать некоторый уровень здравомыслия.

Здесь мы начинаем говорить о том, «почему контейнеры»?

Почему, да это так.

Теперь у меня новый подход к тестированию: контролируемые компоненты. Я знаю, что управляемые компоненты, контейнеры и т. д. на данный момент довольно устарели. А если серьезно, то старенький, но добрый. Если вы избавитесь от как можно большего количества асинхронных взаимодействий, абстрагировав их из своего представления, вы сможете протестировать различные статические состояния вашего пользовательского интерфейса, а затем сделать вывод, работает ли ваш компонент так, как задумано.

Вам нужна перекомпоновка для создания компонентов более высокого порядка (HOC) или какой-то фреймворк для этого?

Технически нет. В качестве примера созданного вручную HOC взгляните на контейнер withApiHandlers в примерах кода в конце этого поста.

Давайте быстро определим HOC. Я постоянно слышу, как люди описывают контейнеры, сделанные с помощью recompose, как HOC. Но затем я слышу, как те же самые люди приходят в замешательство, если кто-то использует HOC для описания компонента, который использует реквизиты рендеринга для достижения того же разделения задач.

Я должен покончить с этим беспорядком раз и навсегда. Проведем аналогию с функцией высшего порядка. Функция высшего порядка — это функция, которая принимает другую функцию в качестве аргумента. Например, в JavaScript метод массива map является функцией более высокого порядка. Это функция, которая принимает функцию, которая затем используется для проецирования массива A в и массива B.

Учитывая это, что такое компонент более высокого порядка? Это компонент, который принимает компонент в качестве аргумента и что-то с ним делает. Это своего рода общий способ составления компонентов, чтобы что-то произошло. Мне нравится говорить, что это способ начальной загрузки компонентов, чтобы дать им сверхспособности, которых у них нет самих по себе.

Сила начальной загрузки HOC имеет первостепенное значение для решения наших задач тестирования. Как только я удалил асинхронный характер из своего пользовательского интерфейса, я сэкономил часы времени, которые были бы потрачены на то, чтобы эти тесты были просто правильными. Мои намерения были правильными, но в конечном итоге я потратил свою энергию на создание тестов, которым суждено было сломаться. Пользовательский интерфейс, разбитый на ряд статических состояний, поддерживается в долгосрочной перспективе. Это упрощает написание для них сборников рассказов и их модульное тестирование.

Одна вещь, которую я усвоил, работая разработчиком со временем, заключается в том, что вы не можете полностью полагаться на проекты с открытым исходным кодом, чтобы помочь вам создать стабильный продукт. Вас что-то облажает, и это стресс, который мне просто не нужен. Это то, с чем вы можете бороться, делая разумный выбор, который приводит к более удобному для сопровождения коду.

Давайте рассмотрим пример преобразования интерактивного компонента пользовательского интерфейса в набор элементов без сохранения состояния, которые могут поддерживать историю тестирования, которую можно поддерживать в долгосрочной перспективе. Вот репо. Это приложение, которое показывает твиты политиков. Пользователь щелкает раскрывающийся список, чтобы выбрать твиты политика, которые он хочет просмотреть.

Вот наша реализация «до» основного компонента представления этого приложения:

//// mock data
const TWEET_MAP: { [key: string]: string[] } = {
  trump: ['Mexico', 'China', 'Kittens'],
  obama: ['Change', 'Dreamers', 'Obamacare'],
};
//// view component
class View extends React.Component<{}, State> {
  state = {    
    selectedPolitician: undefined,    
    tweets: [],    
    loading: false,  
  };   
  /**  
   * Both the visualized tweets and the loading state are a product         
   * of the asynchronous `getPoliticianTweets` operation which is triggered by  
   * `selectPolitician`   
   *  
   * Our refactoring goal is to separate `View` into pieces such that  
   * this asynchronous operation will be represented in "spirit" but not in  
   * our tests in practice.  
   */  
selectPolitician = (value: Politician) => {    
    const selectedPolitician: Politician = value;     
    this.setState({ loading: true, selectedPolitician });    
    getPoliticianTweets(selectedPolitician, (tweets: string[]) => {        
      this.setState({ loading: false, tweets });  
    }); 
   }
  render() {    
    const { loading, tweets, selectedPolitician } = this.state;    
    return (     
      <MuiThemeProvider>        
        <Card>
          <CardHeader title={<span>Tweets for...
              <PoliticianSelector
                onSelectPolitician={this.selectPolitician}
                selectedPolitician={selectedPolitician}
              />
            </span>}
          />
          <CardText>
            {loading ? <Loader /> : tweets.map((tweet: string) => <div key={tweet}>{tweet}</div>)}
          </CardText>
        </Card>
      </MuiThemeProvider>
    );
  }
}

Если бы мы оставили реализацию в таком виде, в тесте нам пришлось бы имитировать вызов API getPoliticianTweets, что достаточно просто, но не так уж сложно. Что сложно, так это найти правильные элементы для имитации кликов, надеясь, что вы сможете точно определить время с вашими утверждениями, а также тот факт, что иногда библиотеки компонентов пользовательского интерфейса для React получают доступ к DOM способами, с которыми ваша тестовая библиотека не справляется. изящно. Если вы разложите свою логику пользовательского интерфейса на компоненты без состояния и используете контейнеры для обработки асинхронности, ваша история тестирования станет намного проще.

Ниже приведен пример того, как вы можете реорганизовать свой код для достижения этой цели:

type StateProps = {
  selectedPolitician?: Politician;
  tweets: string[];
  loading: boolean;
};
type HandlerProps = {
  onSelectPolitician: (value?: any) => void;
};
type Props = StateProps & HandlerProps;
//// the view
/**
 * View is now a stateless react element, whose different states are much easier to test than before.
 */
type TweetView = React.SFC<Props>;
export const View: TweetView = ({ onSelectPolitician, loading, tweets, selectedPolitician }) => (
    <Card>
      <CardHeader title={<span>Tweets for...
          <PoliticianSelector
            onSelectPolitician={onSelectPolitician}
            selectedPolitician={selectedPolitician}
          />
        </span>}
      />
      <CardText>
        {loading ? <Loader /> : tweets.map((tweet: string) => <div key={tweet}>{tweet}</div>)}
      </CardText>
    </Card>
);
/// the container
const withApiHandlers = (Component: TweetView) => class extends React.Component<{}, StateProps> {
  state = {
    selectedPolitician: undefined,
    tweets: [],
    loading: false,
  };
/**
   * withApiHandlers contains the state that was once in View and now drives all asynchronous
   * interactions.
   */
  selectPolitician = (value: Politician) => {
    const selectedPolitician: Politician = value;
this.setState({ loading: true, selectedPolitician });
    getPoliticianTweets(selectedPolitician, (tweets: string[]) => {
      this.setState({ loading: false, tweets });
    });
  }
render() {
    const { loading, tweets, selectedPolitician } = this.state;
    return (
      <Component
        loading={loading}
        tweets={tweets}
        selectedPolitician={selectedPolitician}
        onSelectPolitician={this.selectPolitician}
      />
    );
  }
};
/// wrapping the view in with the container
export default withApiHandlers(View);

Теперь, когда основная логика пользовательского интерфейса была включена в элемент React без сохранения состояния, пользовательский интерфейс, который он выводит, буквально является функцией его входных данных. Вы можете довольно легко выразить все его состояния в наборе тестов. Тесты будут стабильными и довольно простыми в написании. Пожалуйста, ознакомьтесь с тестами, которые я написал для этой логики представления, здесь.

Да, и прежде чем вы уйдете, вот изображение приложения в действии: