Pull to refresh

Построение надежных веб-приложений на React: Часть 3, тестирование с Jasmine

Website development *IT systems testing *JavaScript *
Translation
Tutorial
Original author: Matt Hinchliffe
Перевод статьи «Building robust web apps with React: Part 3, testing with Jasmine», Matt Hinchliffe

От переводчика: это перевод третьей части цикла статей «Building robust web apps with React»
Переводы:


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


Настройка тестирования


Я использую тестовый фреймворк Jasmine, так как он прост в установке и широко используется, в том числе в библиотеке React. Приложение теперь содержит папку test, с двумя директориями; в папке lib скрипты для запуска тестов и папка spec, в которой находятся сами тесты:

tube-tracker/
├── app/
├── public/
└── test/
    ├── lib/
    │   └── jasmine-2.0.0/
    ├── spec/
    │   ├── common/
    │   ├── component/
    │   ├── bundle.js
    │   └── suite.js
    └── SpecRunner.html


В дополнение к окружениям разработки и продакшна, которые я описал в предыдущей части, я добавил тестовое окружение, для того чтобы связать вместе приложение и тесты. Чтобы сделать это, я включил все файлы тестов(specs) в файл suite.js и использовал его, как входную точку для Browserify:

$ browserify -e test/spec/suite.js -t reactify -o test/spec/bundle.js

Создание тестового окружения может быть улучшено при помощи некоторой дополнительной автоматизации, но базовый процесс и так работает. Простота установки также означает то, что тесты запускаются в браузере, а не в специальном окружении, как например jsdom, что я и предпочитаю.

Примечание: Я переключился с использования Bower на NPM дистрибутив React. В Bower версии React'а утилиты для тестирования и другие дополнения поставляются вместе с ядром библиотеки, а это значит, что ядро может быть дважды включено в тестовом окружении. Это вызывает конфликты между компонентами объявленными в разных пакетах. Использование дистрибутива NPM позволяет Browserify построить каждый пакет только с необходимыми ему зависимостями, избегая дублирования.

Тестирование компонентов React


Если считать, что React это V(представление) в MVC, то, теоретически, должен тестироваться только вывод компонентов, но компоненты React зачастую содержат логику для обработки динамического поведения, а простые приложения могут только из них и состоять. Для примера, внутри компонентов приложения Tube Tracker содержится логика для валидации ввода пользователя, установка AJAX пула(poll) и отображение состояния. Следовательно, тестирование одного вывода не предоставит достаточно информации, если внутри что-то сломается, так что тестирование внутренней реализации также необходимо.

Тестовые инструменты React

Чтобы немного облегчить тестирование React компонентов, разработчики React предоставили инструменты для тестирования (TestUtils). Дополнение, которое, вероятно, будет первым, что вы найдете поискав информацию о тестировании React приложений. Оно может быть использовано, подключив в тестовые файлы пакет React с аддонами. В пространстве имен React.addons.TestUtils содержатся методы для симуляции событий, выборки по компонентам и тестирования их типов.

Есть очень полезный метод renderIntoDocument, который может рендерить компоненты в анонимный DOM узел, но для некоторых тестов все же остается необходимость указывать контейнер, например для захвата событий или тестирования жизненного цикла компонента при его уничтожении:

describe("A component", function() {

  var instance;
  var container = document.createElement("div");

  afterEach(function() {
    if (instance && instance.isMounted()) {
      // Only components with a parent will be unmounted
      React.unmountComponentAtNode(instance.getDOMNode().parent);
    }
  });

  describe("rendered without a container reference", function() {
    beforeEach(function() {
      // This component does not use any lifecycle methods or broadcast
      // events so it does not require rendering to the DOM to be tested.
      instance = TestUtils.renderIntoDocument(<ComponentAlpha title="Hello World" />);
    });

    it("should render a heading with the given text", function() {
      // TestUtils provide methods to filter the rendered DOM so that
      // individual components may be inspected easily.
      var heading = TestUtils.findRenderedDOMComponentWithTag(instance, "h1");
      expect(heading.getDOMNode().textContent).toBe("Hello World");
    });
  });

  describe("with a container reference required", function() {
    beforeEach(function() {
      // This component broadcasts events and has lifecycle methods
      // so it should be rendered into an accessible container.
      instance = React.renderComponent(<ComponentBeta />, container);

      this.eventSpy = jasmine.createSpy();
      container.addEventListener("broadcast", this.eventSpy, false);
    });

    afterEach(function() {
      container.removeEventListener("broadcast", this.eventSpy, false);
    });

    it("should broadcast with data when component is clicked", function() {
      // TestUtils can simulate events
      TestUtils.Simulate.click(instance.getDOMNode());
      expect(this.eventSpy).toHaveBeenCalledWith("some", "data");
    });
  });
});

TestUtils очень упрощает взаимодействие и тестирование вывода компонентов, но это не касается исследования их внутренней реализации.

Исследование реализации компонентов

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

image
Приложение Tube Tracker содержит компоненты до четырех уровней вложенности, и большая часть логики приложения находится внутри них.

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

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

Изолирование CommonJS модулей

Нам нужно тестировать каждый модуль изолированно, так как работа со всем древом компонентов может быть неэффективной при отладке ошибок и приводит к тому, что тесты работают не вполне независимо. Проблема в том, что модули CommonJS создают свою собственную область видимости и только к их публичным свойствам, можно обратиться из зависимых компонентов. Это порождает проблему с тестированием, так как зависимости модуля не всегда объявлены публичными. Например в приложении Tube Tracker компонент tube-tracker.js содержит зависимости network.js и predictions.js:
 
/** @jsx React.DOM */
var React = require("react");
var Predictions = require("./predictions");
var Network = require("./network");

var TubeTracker = React.createClass({
  render: function() {
    return (
      <div className="layout">
        <div className="layout__sidebar">
          <Network networkData={this.props.networkData} />
        </div>
        <div className="layout__content">
          <Predictions line={this.state.line} station={this.state.station} networkData={this.props.networkData} />
        </div>
      </div>
    );
  }
});

module.exports = TubeTracker;

Чтобы обойти нехватку видимости, я могу модифицировать модули таким образом, чтобы ихние зависимости поставлялись им извне, вместо того, чтобы быть созданными внутри них, это базовый шаблон инверсии зависимостей (IoC). Без какого-то способа внедрения зависимостей(dependency injection), использование шаблона IoC может привести к спагетти зависимостям. Но внедрение зависимостей не очень-то популярная вещь в JavaScript проектах, так как оно требует строгого следования соглашениям, а её реализация бывает очень разной.

К счастью, есть множество более простых способов проникновения и замены CommonJS модулей. Для node.js существует Rewire, браузерная версия этого инструмента может быть построена трансформацией Rewireify доступной для Browserify:

$ npm install --save-dev rewireify
$ browserify -e test/spec/suite.js -t reactify -t rewireify -o test/spec/bundle.js

Rewireify очень прост, он внедряет __get__ и __set__ методы в каждый модуль, чтобы их внутренние свойства могли быть доступны извне. Зависимости модулей теперь могут быть заменены стабами:

/** @jsx React.DOM */
var React = require("react/addons");
var TubeTracker = require("../../../app/component/tube-tracker");
var stubComponent = require("../../lib/stub/component");

describe("Tube Tracker", function() {
  var TestUtils = React.addons.TestUtils;

  beforeEach(function() {
    this.original = {
      network: TubeTracker.__get__("Network"),
      predictions: TubeTracker.__get__("Predictions")
    };

    this.stubbed ={
      network: stubComponent(),
      predictions: stubComponent()
    };

    TubeTracker.__set__({
      Network: this.stubbed.network,
      Predictions: this.stubbed.predictions
    });
  });

  afterEach(function() {
    TubeTracker.__set__({
      Network: this.original.network,
      Predictions: this.original.predictions
    });
  });
});


Подмена зависимостей теперь очень проста, но компоненты нуждаются в особом обращении. TestUtils предоставляет метод mockComponent, который позволяет подменивать вывод переданного компонента, но это, в основном и все, что он может делать. На самом деле, иногда удобнее подменять целые компоненты, особенно для асинхронных тестов.

Jest, недавно созданная командой Facebook обертка для Jasmine, это альтернативный способ подмены зависимостей CommonJS. Документация по использованию Jest с React доступна здесь.

Асинхронное тестирование компонентов

Не все тесты можно заставить выполняться синхронно, в случае приложения Tube Tracker, компонент Predictions будет всегда показывать экземпляр Message перед отображением DepartureBoard. Отсутствие возможности проследить(spy) или подменить(stub) методы жизненного цикла компонента, например componentDidMount или componentWillUnmount, является проблемой, так как вы не сможете узнать, когда компонент создится или разрушится.

Чтобы обойти это ограничение, я создал функцию для обеспечения лучшей подмены компонентов. Функция принимает обратные вызовы для методов жизненного цикла, таким образом становится очень удобно вставлять обратные вызовы при выполнении тестов:

/** @jsx React.DOM */
var React = require("react");

module.exports = function stub(mount, unmount) {
  var mixins = [];

  if (mount) {
    mixins.push({
      componentDidMount: function() {
        mount.call(this);
      }
    });
  }

  if (unmount) {
    mixins.push({
      componentWillUnmount: function() {
        unmount.call(this);
      }
    });
  }

  return React.createClass({
    mixins: mixins,
    render: function() {
      return <div />;
    }
  });
};

Итог


Тестирование моих React приложений оказалось намного сложнее, чем я ожидал. Это новая технология и мы до сих пор учимся, как ее лучше всего использовать. Я должен был создать Rewireify и я потратил много времени на изучение внутренностей React’а. Я не говорю, что все что я сделал, это лучшие практики, но есть не так много информации о том, как это должно работать. Самое главное, что это работает:

image

Вы можете попробовать приложение прямо сейчас (внимание: пример запущен на бесплатном аккаунте, так что эта ссылка может быть неустойчивой) или пройти на GitHub, чтобы посмотреть исходный код. Пожалуйста, комментируйте или твитайте мне, я буду рад получить отзывы.
Tags:
Hubs:
Total votes 7: ↑5 and ↓2 +3
Views 15K
Comments Comments 2