Побудова надійних веб-додатків на React: Частина 3, тестування з Jasmine

      Переклад статті «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, щоб подивитися вихідний код . Будь ласка, коментуйте або твітайте мені , я буду радий отримати відгуки.
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.