Статьи

Тестирование компонентов в React с использованием Jest и Enzyme

Это вторая часть серии «Тестирование компонентов в React». Если у вас есть опыт работы с Jest, вы можете пропустить его и использовать код GitHub в качестве отправной точки.

В предыдущей статье мы рассмотрели основные принципы и идеи, лежащие в основе разработки через тестирование. Мы также настроили среду и инструменты, необходимые для запуска тестов в React. В набор инструментов входили Jest, ReactTestUtils, Enzyme и рендеринг реагирующих тестов.

Затем мы написали пару тестов для демонстрационного приложения с использованием ReactTestUtils и обнаружили его недостатки по сравнению с более надежной библиотекой, такой как Enzyme.

В этом посте мы получим более глубокое понимание компонентов тестирования в React, написав более практичные и реалистичные тесты. Вы можете отправиться на GitHub и клонировать мой репо, прежде чем начать.

Enzyme.js — это библиотека с открытым исходным кодом, поддерживаемая Airbnb, и это отличный ресурс для разработчиков React. Он использует API ReactTestUtils, но в отличие от ReactTestUtils, Enzyme предлагает API высокого уровня и простой для понимания синтаксис. Установите энзим, если вы еще этого не сделали.

Enzyme API экспортирует три типа параметров рендеринга:

  1. мелкий рендеринг
  2. полный рендеринг DOM
  3. статический рендеринг

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

1
2
3
4
5
import { shallow } from ‘enzyme’;
import ProductHeader from ‘./ProductHeader’;
 
// More concrete example below.
 const component = shallow(<ProductHeader/>);

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

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

Вот тесты, которые мы написали в последнем уроке:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import ReactTestUtils from ‘react-dom/test-utils’;
 
describe(‘ProductHeader Component’, () => {
    it(‘has an h2 tag’, () => {
 
      const component = ReactTestUtils
                            .renderIntoDocument(<ProductHeader/>);
      var node = ReactTestUtils
                    .findRenderedDOMComponentWithTag(
                     component, ‘h2’
                    );
     
  });
 
    it(‘has a title class’, () => {
 
      const component = ReactTestUtils
                            .renderIntoDocument(<ProductHeader/>);
      var node = ReactTestUtils
                    .findRenderedDOMComponentWithClass(
                     component, ‘title’
                 );
    })
  })

Первый тест проверяет, есть ли у компонента ProducerHeader тег <h2> , а второй — на наличие класса CSS с именем title . Код трудно читать и понимать.

Вот тесты, переписанные с использованием Enzyme.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import { shallow } from ‘enzyme’
 
describe(‘ProductHeader Component’, () => {
 
    it(‘has an h2 tag’, () => {
      const component = shallow(<ProductHeader/>);
      var node = component.find(‘h2’);
      expect(node.length).toEqual(1);
      
  });
 
    it(‘has a title class’, () => {
      const component = shallow(<ProductHeader/>);
      var node = component.find(‘h2’);
      expect(node.hasClass(‘title’)).toBeTruthy();
    })
  })

Во-первых, я создал поверхностно- <ProductHeader/> DOM компонента <ProductHeader/> используя shallow() и сохранил его в переменной. Затем я использовал метод .find() чтобы найти узел с тегом ‘h2’. Он запрашивает DOM, чтобы увидеть, есть ли совпадение. Поскольку существует только один экземпляр узла, мы можем смело предположить, что node.length будет равна 1.

Второй тест очень похож на первый. Метод hasClass('title') возвращает информацию о том, имеет ли текущий узел реквизит className со значением title. Мы можем проверить правдивость, используя toBeTruthy() .

Запустите тесты, используя yarn test , и оба теста должны пройти.

Отлично сработано! Теперь пришло время провести рефакторинг кода. Это важно с точки зрения тестера, потому что читаемые тесты легче поддерживать. В приведенных выше тестах первые две строки идентичны для обоих тестов. Вы можете beforeEach() их, используя функцию beforeEach() . Как следует из названия, функция beforeEach один раз перед выполнением каждой спецификации в блоке описания.

Вы можете передать функцию стрелки в beforeEach() следующим образом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import { shallow } from ‘enzyme’
 
describe(‘ProductHeader Component’, () => {
    let component, node;
     
    // Jest beforeEach()
    beforeEach((()=> component = shallow(<ProductHeader/>) ))
    beforeEach((()=> node = component.find(‘h2’)) )
     
    it(‘has an h2 tag’, () => {
        expect(node).toBeTruthy()
    });
 
    it(‘has a title class’, () => {
      expect(node.hasClass(‘title’)).toBeTruthy()
    })
})

Давайте напишем несколько модульных тестов для компонента ProductDetails . Это презентационный компонент, который отображает детали каждого отдельного продукта.

Тестирование компонентов в React - компонент ProductDetails выделен
Мы собираемся протестировать выделенный раздел

Модульный тест попытается утвердить следующие предположения:

  • Компонент существует, и реквизит передается.
  • Отображаются реквизиты, такие как название продукта, описание и доступность.
  • Сообщение об ошибке отображается, когда реквизит пуст.

Вот голая структура теста. Первый beforeEach() сохраняет данные о продукте в переменной, а второй монтирует компонент.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
describe(«ProductDetails component», () => {
    var component, product;
 
    beforeEach(()=> {
        product = {
            id: 1,
            name: ‘NIKE Liteforce Blue Sneakers’,
            description: ‘Lorem ipsum.’,
            status: ‘Available’
        };
    })
    beforeEach(()=> {
        component = mount(<ProductDetails product={product} foo={10}/>);
    })
 
    it(‘test #1’ ,() => {
      
    })
})

Первый тест прост:

1
2
3
4
it(‘should exist’ ,() => {
     expect(component).toBeTruthy();
     expect(component.props().product).toEqual(product);
})

Здесь мы используем метод props() который удобен для получения реквизита компонента.

Для второго теста вы можете запросить элементы по именам их классов, а затем проверить, являются ли имя продукта, описание и т. Д. innerText этого элемента.

1
2
3
4
5
6
7
8
it(‘should display product data when props are passed’, ()=> {
      let title = component.find(‘.product-title’);
      expect(title.text()).toEqual(product.name);
       
      let description = component.find(‘.product-description’);
      expect(description.text()).toEqual(product.description);
       
   })

Метод text() особенно полезен в этом случае для извлечения внутреннего текста элемента. Попробуйте написать ожидание для product.status() и посмотрите, проходят ли все тесты.

Для финального теста мы собираемся смонтировать компонент ProductDetails без каких-либо подпорок. Затем мы будем искать класс с именем .product-error и проверять, содержит ли он текст «Извините, продукт не существует».

1
2
3
4
5
6
7
it(‘should display an error when props are not passed’, ()=> {
       /* component without props */
       component = mount(<ProductDetails />);
 
       let node = component.find(‘.product-error’);
       expect(node.text()).toEqual(‘Sorry. Product doesnt exist’);
   })

Вот и все. Мы успешно протестировали компонент <ProductDetails /> отдельно. Тесты этого типа известны как модульные тесты.

Мы только что узнали, как тестировать реквизит. Но чтобы действительно протестировать компонент изолированно, вам также необходимо протестировать функции обратного вызова. В этом разделе мы напишем тесты для компонента ProductList и создадим заглушки для функций обратного вызова. Вот предположения, которые мы должны утверждать.

Тестирование компонентов в React - ProductList Component
  1. Количество перечисленных продуктов должно быть эквивалентно количеству объектов, которые компонент получает в качестве реквизита.
  2. Нажатие на <a> должно вызвать функцию обратного вызова.

Давайте создадим функцию beforeEach() которая заполняет фиктивные данные о продукте для наших тестов.

01
02
03
04
05
06
07
08
09
10
11
12
beforeEach( () => {
        productData = [
           {
               id: 1,
               name: ‘NIKE Liteforce Blue Sneakers’,
               description: ‘Lorem ipsu.’,
               status: ‘Available’
        
           },
          // Omitted for brevity
       ]
   })

Теперь давайте смонтируем наш компонент в другом beforeEach() .

1
2
3
4
5
6
7
8
9
beforeEach(()=> {
    handleProductClick = jest.fn();
    component = mount(
                    <ProductList
                        products = {productData}
                        selectProduct={handleProductClick}
                    />
                );
})

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

Заглушка — это фиктивная функция, которая притворяется другой функцией. Это позволяет вам независимо тестировать компонент без импорта родительских или дочерних компонентов. В приведенном выше примере мы создали функцию-заглушку с именем handleProductClick , вызвав jest.fn() .

Теперь нам нужно найти все элементы <a> в DOM и смоделировать щелчок по первому узлу <a> . После нажатия мы проверим, был ли handleProductClick() . Если да, будет справедливо сказать, что наша логика работает, как ожидалось.

1
2
3
4
5
6
7
8
it(‘should call selectProduct when clicked’, () => {
 
    const firstLink = component.find(‘a’).first();
    firstLink.simulate(‘click’);
    expect(handleProductClick.mock.calls.length).toEqual(1);
 
    })
})

Фермент позволяет легко моделировать действия пользователя, такие как щелчки, с помощью метода simulate() . handlerProductClick.mock.calls.length возвращает количество раз, когда была вызвана handlerProductClick.mock.calls.length функция. Мы ожидаем, что он будет равен 1.

Другой тест относительно прост. Вы можете использовать метод find() для извлечения всех узлов <a> в DOM. Количество узлов <a> должно быть равно длине массива productData, который мы создали ранее.

1
2
3
4
5
it(‘should display all product items’, () => {
    
       let links = component.find(‘a’);
       expect(links.length).toEqual(productData.length);
   })

Далее мы собираемся протестировать компонент ProductContainer . У него есть состояние, хук жизненного цикла и метод класса. Вот утверждения, которые необходимо проверить:

  1. componentDidMount вызывается ровно один раз.
  2. Состояние компонента заполняется после монтирования компонента.
  3. Метод handleProductClick() должен обновлять состояние, когда идентификатор продукта передается в качестве аргумента.

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

Вы можете следить за функцией следующим образом:

1
2
3
4
5
it(‘should call componentDidMount once’, () => {
       componentDidMountSpy = spyOn(ProductContainer.prototype,
                              ‘componentDidMount’);
       //To be finished
   });

Первый параметр jest.spyOn — это объект, который определяет прототип класса, за которым мы следим. Второй — это название метода, который мы хотим шпионить.

Теперь визуализируйте компонент и создайте утверждение, чтобы проверить, был ли вызван шпион.

1
2
component = shallow(<ProductContainer/>);
    expect(componentDidMountSpy).toHaveBeenCalledTimes(1);

Чтобы проверить, что состояние компонента заполняется после монтирования компонента, мы можем использовать метод state() Enzyme, чтобы получить все в состоянии.

1
2
3
4
5
6
it(‘should populate the state’, () => {
       component = shallow(<ProductContainer/>);
       expect(component.state().productList.length)
           .toEqual(4)
 
   })

Третий немного сложнее. Нам нужно убедиться, что handleProductClick работает handleProductClick . Если вы handleProductClick() к коду, то увидите, что метод handleProductClick() принимает идентификатор продукта в качестве входных данных, а затем обновляет this.state.selectedProduct сведения об этом продукте.

Чтобы проверить это, нам нужно вызвать метод компонента, и вы можете сделать это, вызвав component.instance().handleProductClick() . Мы передадим образец продукта ID. В приведенном ниже примере мы используем идентификатор первого продукта. Затем мы можем проверить, было ли обновлено состояние, чтобы подтвердить, что утверждение верно. Вот весь код:

1
2
3
4
5
6
7
8
it(‘should have a working method called handleProductClick’, () => {
       let firstProduct = productData[0].id;
       component = shallow(<ProductContainer/>);
       component.instance().handleProductClick(firstProduct);
 
       expect(component.state().selectedProduct)
           .toEqual(productData[0]);
   })

Мы написали 10 тестов, и если все пойдет хорошо, вот что вы должны увидеть:

Окончательный вывод с прохождением тестов

Уф! Мы рассмотрели практически все, что вам нужно знать, чтобы начать писать тесты в React с использованием Jest и Enzyme. Сейчас самое время заглянуть на сайт Enzyme, чтобы глубже взглянуть на их API.

Что вы думаете о написании тестов в React? Я хотел бы услышать их в комментариях.