Серия модульных тестов React (Часть 1)

Этот блог будет состоять из двух частей:

  • Основы модульного тестирования, фреймворков тестирования и библиотек взаимодействия с DOM
  • Развенчание мифов о фреймворке для тестирования Jest. (Еще впереди)

Поскольку ваше веб-приложение растет вместе с растущими бизнес-требованиями, модульное тестирование является обязательным для поддержания работоспособности внешнего кода. У нас есть несколько фреймворков в нашем интерфейсном мире, у нас также есть множество фреймворков и технологий для тестирования. Тот, кто начинает с фронтенд-разработки, может легко запутаться во всем доступном материале. Этот блог - небольшая попытка представить модульное тестирование более простым способом. Итак, приступим.

Методы тестирования

В основном есть два способа, с помощью которых мы можем покрыть критически важные функции нашего внешнего кода.

Модульное тестирование

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

Интеграция / функциональное тестирование

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

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

Модульное тестирование

Давайте углубимся и попробуем понять, что на самом деле представляет собой единица.

Единица

Модульный тест - это автоматизированный фрагмент кода, который вызывает тестируемую единицу работы, а затем проверяет некоторые предположения об одном конечном результате этой единицы. Модульный тест почти всегда пишется с использованием среды модульного тестирования. Его легко писать, и он работает быстро. Он заслуживает доверия, читается и обслуживается. Результаты стабильны до тех пор, пока не изменился производственный код. - Книга Роя Ошерова «Искусство модульного тестирования».

Независимо от того, пишем ли мы наше приложение в объектно-ориентированном программировании или в функциональном программировании, единственный метод в классе или модуле можно рассматривать как тестируемую единицу. Предполагается, что модуль возвращает значение для заданного набора входов, обрабатывает сбои или даже вызывает другую функцию / метод / процедуру из in. Давайте рассмотрим этот пример:

// Person class
class Person {
    sayHello (name) {
        console.log("Hello" + name + "!");
    }
}

// sayHello test cases
// - it should print "Hello JS!” if sayHello is called with “JS” => PASSED
// - it should print “Hello World!” if sayHello is called with no parameters => FAILED

В приведенном выше примере мы можем легко рассматривать метод sayHello как единое целое и тестировать его поведение индивидуально. Учитывая, что мы находимся на одной странице, давайте продвинемся дальше в понимании того, что нам нужно учитывать при написании модульных тестов.

Модульный тест

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

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

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

Кроме того, сложный модуль может иметь несколько зависимостей, которые он использует для выполнения задачи, которую он намерен выполнить. Рассмотрим компонент React, отображающий форму с входным значением и кнопкой отправки. При отправке формы выполняется вызов API для сохранения значения ввода в базе данных.

class Form extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            input: "",
        };

        this.onChangeHandler = this.onChangeHandler.bind(this);
        this.onSubmitHandler = this.onSubmitHandler.bind(this);
    }
    
    onChangeHandler(event) {
        this.setState({
            input: event.target.value
        });
    }

    onSubmitHandler(event) {
        Axios.post("some_url", {data: this.state.input})
        .then(response => {
            AlertsHelper.show({
                type: "success",
                message: response.message,
            });
        })
        .catch(error => {
            AlertsHelper.show({
                type: "error",
                message: error.message,
            });
        });
    }
    render () {
        return (
            <form onSubmit={this.onSubmitHandler}>
                <input type=“text” 
                       onChange={this.onChangeHandler} />
                <button type=“submit”>Submit</button>
            </form>
        );
    }
}

Теперь этот обработчик отправки представляет собой единицу, и его задача - выполнить вызов API для сохранения данных, предупреждения об успешном завершении и сообщения об ошибке при сбое. Этот обработчик использует несколько зависимостей, например, библиотеку «Axios» для вызова API, библиотеку «AlertsHelper» для отображения предупреждающих сообщений. Но когда мы собираемся написать модульный тест для этого конкретного модуля, мы не должны тестировать поведение «Axios» и «AlertsHelper», поскольку это зависимости, используемые рассматриваемым модулем. В задачу этого модульного теста не входит проверка того, прошел ли запрос POST и смог ли «AlertsHelper» показать предупреждающее сообщение или нет. Таким образом, мы можем смоделировать эти зависимости, чтобы они работали по мере необходимости, и можем протестировать различные аспекты рассматриваемого модуля.

Мы можем имитировать Axios.post, чтобы он был успешным, чтобы проверить успешный случай, или быть неудачным, чтобы проверить случай неудачи. Нас не беспокоит то, что происходит в «AlertHelpers.show», но все, что нас волнует, - это то, что «onSubmitHandler» должен вызывать его с разными данными в зависимости от успеха или неудачи. Я надеюсь, что это дает представление о том, как отличить зависимости от фактического модуля и сделать модульный тест независимым от функциональности этих зависимостей. Забегая вперед, давайте поговорим о фреймворках для модульного тестирования.

Платформа модульного тестирования

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

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

Давайте рассмотрим некоторые фреймворки для модульного тестирования. Я расскажу в основном о двух из них - Mocha и Jest.

Мокко

Это самый известный и используемый фреймворк для тестирования в мире JS, пользующийся очень активной поддержкой сообщества. Как они говорят, это просто, гибко и весело. Он может запускать тесты как для браузерных, так и для узловых приложений. Он предоставляет пользователю различные варианты выбора библиотеки утверждений, библиотеки имитации, шпионов и так далее. Это дает пользователю возможность создать пакет на основе своих потребностей и настроить его на ходу. При написании тестов для компонента React вам также потребуется настроить виртуальную DOM с использованием библиотеки JSDOM. Вот отличная статья, которая поможет вам настроить mocha для приложения React.

Шутка

Это рекомендуемый способ тестирования приложений React, разработанный Facebook, безболезненный фреймворк для тестирования javascript. Он основан на знаменитом фреймворке тестирования Jasmine, который имеет собственную настройку для утверждений, шпионов, заглушек и моков. Jest не дает возможности выбрать библиотеку утверждений или имитаций по вашему выбору, но в нем все предварительно настроено. В этом есть свои плюсы и минусы. Если вам нужна гибкость и выбор в настройке, определенно Jest не тот, но если вам нужна быстрая настройка, чтобы начать писать тесты, Jest определенно будет хорошим вариантом. Более того, он также устанавливает для вас поддельный DOM, поэтому библиотека JSDOM не нужна. Вы можете настроить Jest с помощью руководства Приступая к работе от Facebook.

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

Давайте разберемся с этим на примере:

import {add} from "helpers"

const calculator = (a, b, op) => {
  if (op == "+") 
    return add (a, b);
}

/*****************************************************************
When we unit test calculator, we are concerned with whether
calculator calls add helper or not if `op` is `+` and not with
whether calculator returns sum of `a` and `b` if op is `+`

When we test this with Jest, `add` from helpers is automatically
mocked to return undefined and we shouldn't be concerned about it.
And as it is auto mocked, we can easliy spy on it as below
*******************************************************************/

describe("Calculator", () => {
  it("should call 'add' helper, if op is '+'", () => {
    // Given
    let a = 10;
    let b = 2;
    let op = "+";
    
    // When
    const result = calculator(a, b, op);
    
    // Then
    expect(add).toBeCalledWith(a, b);
  })
});

Библиотеки взаимодействия с DOM

Забегая вперед, давайте немного поговорим о библиотеках взаимодействия с DOM. Как следует из названия, библиотеки взаимодействия с DOM помогают получить доступ к компонентам с заданным именем класса, именем тега или типом. Они позволяют очень легко тестировать компоненты React, упрощая манипуляции, обход или доступ к элементам из выходных данных React Component.

Facebook предоставляет библиотеку ReactTestUtils для взаимодействия с DOM. Еще одна замечательная библиотека - Enzyme от Airbnb, которая в основном использует jQuery API для управления и обхода DOM. Эти библиотеки не зависят от исполнителей тестов, что означает, что мы можем использовать любую комбинацию - Jest + Enzyme / Jest + TestUtils / Mocha + Enzyme / Mocha + TestUtils. Обе библиотеки поддерживают поверхностный рендеринг и полный рендеринг DOM.

Ниже приводится простой компонент React и его тест с использованием Jest и Enzyme:

export const Form = ({name, age, onSubmit, onUpdate}) => {
  return (
    <form onSubmit={onSubmit}>
      <div className="form-group">
        <label className="control-label">Name</label>
        <div className="controls">
          <input type="text" 
                 className="input-md form-control" 
                 id="name" 
                 name="name"
                 value={name} 
                 onChange={onUpdate.bind(null, "name")}/>
        </div>
      </div>
      <div className="form-group">
        <label className="control-label">Age</label>
        <div className="controls">
          <input type="number" 
                 className="input-md form-control" 
                 id="age" 
                 name="age"
                 onChange={onUpdate.bind(null, "age")}/>
        </div>
      </div>
      <div className="controls clearfix">
        <button type="submit" 
                className="btn btn-primary btn-md btn-block">
          Submit
        </button>
      </div>
    </form>
  );
}
// Form.test.js
describe('PageForm', () => {
  let form;
  const props = {
    name: 'Xyz',
    age: 20,
    onSubmit: jest.fn(),
    onUpdate: jest.fn()
  }

  beforeEach(() => {
    form = shallow(<Form {...props} />);
  });

  it('should exists', () => {
    // Given

    // When

    // Then
    expect(form.length).toEqual(1);
  });

  it('should render name input', () => {
    // Given

    // When
    let nameInput = form.find('input[id="name"]');

    // Then
    expect(nameInput.length).toEqual(1);
  });

  it('should render age input', () => {
    // Given

    // When
    let ageInput = form.find('input[id="age"]');

    // Then
    expect(ageInput.length).toEqual(1);
  });

  it('should render submit button', () => {
    // Given

    // When
    let submitBtn = form.find('button[type="submit"]');

    // Then
    expect(submitBtn.length).toEqual(1);
  });

  it('should call "onUpdate" on change of input', () => {
    // Given

    // When
    let nameInput = form.find('input[id="name"]');
    nameInput.simulate('change');

    // Then
    expect(props.onUpdate).toHaveBeenCalledWith('name');
  });

  it('should call "onSubmit" on form submit', () => {
    // Given

    // When
    let submitBtn = form.find('form');
    submitBtn.simulate('submit');

    // Then
    expect(props.onSubmit).toHaveBeenCalled();
  });
});

Мелкая отрисовка

Неглубокий рендеринг - это концепция рендеринга компонента только «на один уровень», что означает, что у вас нет доступа к поведению дочернего компонента. Вы утверждаете, какой метод рендеринга возвращает. Дочерние компоненты не инициируются и не отображаются. Кроме того, ему не нужна модель DOM, что ускоряет загрузку и выполнение тестов.

Вот простой Page Component, который отображает Form и Output компоненты как свои дочерние.

import React from "react";
import { Form } from './Form';
import { Output } from './Output';
export class Page extends React.Component {
  constructor(props) {
    super(props);
  this.state = {
      name: '',
      age: undefined,
      output: '',
    };
  }
  onSubmit(event) {
    event.preventDefault();
    const {name, age} = this.state;
    const output = `My name is ${name}. I am ${age} years old.`;
    this.setState({output});
  }
  onUpdate(key, event) {
    const state = this.state;
    state[key] = event.target.value;
    this.setState(state);
  }
  render() {
    const {name, age, output} = this.state;
    return (
      <div>
        <Form name={name}
              age={age}
              onSubmit={this.onSubmit.bind(this)}
              onUpdate={this.onUpdate.bind(this)} />
        <Output output={output} />
      </div>
    );
  }
}

Когда мы пишем модульные тесты для вышеуказанного Page компонента, мы рассматриваем только возможность проверить, отображает ли этот компонент Form и Output компоненты или нет, а концепция поверхностного рендеринга позволяет нам проверить это очень быстро. Ниже приведен код для Page.test.js, где я использую API поверхностного рендеринга фермента.

import { shallow } from 'enzyme';
import { Page } from '../../src/components/Page';
import { Form } from '../../src/components/Form';
import { Output } from '../../src/components/Output';
describe('Page', () => {
  let page;
  beforeEach(() => {
    page = shallow(<Page />);
  });
  it('should exists', () => {
    // Given
    // When
    // Then
    expect(page.length).toEqual(1);
  });
  it('should render Form component', () => {
    // Given
    // When
    let formCmp = page.find(Form);
    // Then
    expect(formCmp.length).toEqual(1);
  });
  it('should render Output component', () => {
    // Given
    // When
    let outputCmp = page.find(Output);
    // Then
    expect(outputCmp.length).toEqual(1);
  });

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

Надеюсь, это поможет, и если вам это нравится, пожалуйста 👏. Большое вам спасибо

Продолжай мечтать! Продолжай учиться! Продолжайте делиться. Удачного дня.