Я создаю интерфейсные приложения с помощью React. Я занимаюсь и многими другими вещами. Я создаю серверные системы, например, с помощью Ruby on Rails. Раньше я создавал веб-приложения с использованием RoR, но в настоящее время я все больше и больше работаю с JavaScript, и React со временем стал любимой библиотекой.

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

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

”Outside – in Development фокусируется на удовлетворении потребностей заинтересованных сторон. Основная теория состоит в том, что для создания успешного программного обеспечения команда должна четко понимать цели и мотивацию заинтересованных сторон. Конечная цель - создать программное обеспечение, которое будет потреблять много ресурсов и будет соответствовать потребностям предполагаемого клиента или превосходить их ».

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

Первое, что мы сделаем, - это создадим своего рода пользовательский интерфейс. Почему? Что ж, нам нужно создать точку входа для тех, кто хочет использовать наше приложение. «Но пока нет заявки ...», - скажете вы. Это правильно. Наша работа - построить его.

Мы начнем предполагать, что у нас есть вымышленное приложение, когда мы напишем наш тест, а затем мы воплотим его в жизнь, написав нашу реализацию и построив ее.

Это теория. А теперь займемся этим.

«Внешняя» часть будет покрыта приемочными испытаниями. Иногда их называют сквозными (e2e) тестами. Проще говоря, эти тесты охватывают весь стек и стремятся дать ответ на такие вопросы, как «Если я, как пользователь, заполню это поле некоторым значением и нажму на эту кнопку, то эффект должен быть ...»

Часть «In» (или, скорее, часть «Inside») будет охвачена модульными тестами, цель которых - помочь нам убедиться, что код, который мы пишем (модули, компоненты, службы и т. Д.), Действительно выполняет то, что мы хотим от них. . Есть много способов написания модульных тестов, и мы постараемся сделать их максимально простыми. Для меня важной частью является то, что процесс написания спецификаций (тесты часто называют «спецификациями») помогает мне принимать решения и направляет нас при создании наших модулей.

Цикл приемо-сдаточных испытаний

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

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

Хороший цикл для подражания - это внешний подход:

  1. Напишите пример бизнес-ценности высокого уровня (внешний), который станет красным
  2. Напишите пример нижнего уровня (внутри) для первого шага реализации, который становится красным
  3. Реализуйте минимальный код для передачи этого низкоуровневого примера, чтобы он стал зеленым
  4. Напишите следующий пример более низкого уровня, подталкивающий к прохождению шага 1
  5. Повторяйте шаги 3 и 4, пока тест высокого уровня (1) не станет зеленым.
  6. Начните с написания нового теста высокого уровня

Во время процесса думайте о своем красно-зеленом состоянии как о статусе разрешения:

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

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

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

Начиная

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

Мы можем начать с довольно простого лоу-фай, который поможет нам:

В этом представлении будет отображаться список сотрудников с призывом к действию (CTA) для просмотра сведений об отдельном сотруднике. Нажатие на кнопку добавит наложение к представлению и отобразит модальное окно (или всплывающее окно) с данными о сотруднике. Для дальнейших действий, которые пользователь сможет предпринять (редактировать и удалять сотрудника), должно быть не менее двух призывов к действию. На данный момент, однако, к ним не будет прикреплена никакая функциональность.

Хорошо, давайте создадим первую пользовательскую историю и сценарий для этого представления.

As a user of the application
In order to be able to work with employee management
I would like to access a list of employees.

Хорошо, теперь давайте разберем это на контрольный список вещей, которые должны произойти:

  1. Пользователь получает доступ к приложению через свой браузер.
  2. Приложение извлекает список сотрудников из внутренних систем компании (предоставляется приложению конечной точкой Rest API).
  3. Представление с данными о сотрудниках отображается и отображается в браузере пользователя.

Мы можем начать с этого и написать вторую User Story, как только закончим реализацию этой первой.

Инструменты

Напишем код. Мы решили (или это было решено за нас), что будем использовать ReactJS в этом проекте. Мы будем использовать create-react-app для построения приложения. После создания приложения мы хотим очистить некоторые файлы и удалить большую часть автоматически сгенерированного кода.

$ yarn create react-app employee_management
// or use npm
$ npx create-react-app employee_management

После того как вы очистите файлы проекта, ваша структура должна выглядеть так:

Теперь мы хотим принять некоторые решения относительно фреймворков тестирования, которые мы хотим использовать.

Cypress - это фреймворк для сквозного тестирования Javascript.

Cypress предлагает надежную платформу для написания и автоматизации UI-тестов. Мощные функции, хорошо написанная документация и очень простая конфигурация. Именно так, как нам нравится!

Шаг 1. Установите cypress и сохраните его как зависимость для разработки:

$ yarn add cypress --dev
// or use npm
$ npm i cypress --save-dev

Шаг 2. После завершения установки мы можем запустить Cypress с помощью

$ ./node_modules/.bin/cypress open

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

Шаг 3: Если честно, шага 3 нет… мы закончили. Что ж, не совсем возможно. Мы должны добавить сценарий к нашему package.json, чтобы облегчить доступ к средству выполнения тестов. Я предлагаю вам добавить это в раздел scripts файла package.json:

"cy:open": "yarn start --silent & cypress open"

Я также хотел бы, чтобы вы изменили раздел eslintConfig, чтобы он выглядел следующим образом (мы хотим избежать предупреждений в нашей среде IDE, которые подразумевают, что объект cy не определен):

"eslintConfig": {
  "extends": "react-app",
  "globals": {
    "cy": "cy"
  }
}

Так и должно быть… с точки зрения конфигурации.

Что ж, есть еще кое-что… (всегда есть «еще кое-что»). Cypress поставляется с включенными декларациями типов TypeScript. Современные текстовые редакторы могут использовать эти объявления типов для отображения IntelliSense внутри файлов спецификаций. Самый простой способ увидеть IntelliSense при вводе команды или утверждения Cypress - это добавить директиву с тройной косой чертой в заголовок вашего тестового файла. Это включит IntelliSense для каждого файла. вставьте строку комментария ниже в свой тест (вверху).

/// <reference types="Cypress" />

Хорошо, СЕЙЧАС все готово.

Наш первый тест

Давайте начнем использовать Cypress. Для начала создайте новый тестовый файл в папке cypress/integration. Я буду использовать немного другой стандарт именования, чем тот, который предлагается в примерах (подробнее об этом позже).

$ touch cypress/integration/displayEmployeeList.feature.js

Добавьте describe блок:

describe('Display list of employees', () => {
  // Our tests will go here
})

Пора начинать проверять тесты! Давайте начнем с простого - можем ли мы зайти на сайт и увидеть текст, который мы поместили в section, который мы назвали header?

it('when user visits the page', () => {
    cy.visit('http://localhost:3000')
    cy.get('section[name="header"]')
      .should('contain', 'Employee list')
})

Помните - все блоки it должны находиться внутри блока describe.

Прежде чем запускать наши тесты, нам нужно, чтобы наш локальный хост оставался активным в другом терминале. Запустите его, запустив $ yarn start или $ npm start. (В дальнейшем мы будем вводить только команды yarn - если вы хотите использовать npm, просто используйте его ...)

Запустим Cypress, выполнив скрипт: $ yarn run cy:open. Откроется кипарисовое окно со списком доступных тестов (в нашем случае всего 1 файл). Щелчок по имени файла запустит средство запуска тестов, и откроется новое окно браузера.

Здесь не на что посмотреть, но тест, который мы только что написали, должен пройти.

Показать сотрудников

Итак, наш первый тест пройден. Замечательно. Давайте двигаться дальше.

Мы хотим отобразить детали для 5 сотрудников. Почему 5? Что ж, мы должны что-то решить, и мы хотим контролировать тестовые данные и то, что мы на самом деле показываем пользователю.

Давайте изменим нашу функцию с помощью следующего кода (внутри блока it, который мы добавили ранее):

cy.get('section[name="main"]').within(() => {
  cy.get('li')
    .should('have.length', 5)
})

Если вы изучите сообщение об ошибке, которое выдает бегун, вы увидите следующее сообщение

CypressError: Timed out retrying: Expected to find element: 'section[name="main"] li', but never found it.

Хорошо, давайте подумаем…

Мы хотим отобразить коллекцию пользователей. Итак, давайте рассмотрим выделение этой функции в отдельный компонент. Нам нужно дать компоненту имя. Почему бы не назвать это EmployeeList?

Нам нужно, чтобы этот компонент извлекал данные о сотрудниках из нашей серверной части. Давайте добавим это в наш список TODO. Еще одна вещь, которую нужно добавить к этому TODO, - это то, что мы хотим, чтобы компонент перенумеровал список (<ul></ul>) с элементом списка (<li></li>) для каждого сотрудника ...

Эти элементы списка должны содержать изображение, имя, фамилию и адрес электронной почты… (проверьте lo-fi!)

Хорошо, список TODO растет - было бы неплохо его записать.

Давайте представим на мгновение, что у нас есть готовый и готовый компонент (мы этого не делаем, но мы тестируем код, который нам хотелось бы иметь, не так ли?). Мы можем обновить код нашего приложения, чтобы начать его использовать. Мы добавим import и попросим наш render метод использовать его:

import React, { Component } from 'react';
import EmployeeList from './components/EmployeeList'
class App extends Component {
  render() {
    return (
      <>
        <section name="header">
          <h1>Employee list</h1>
        </section>
        <section name="main">
          <EmployeeList />
        </section>
      </>
    );
  }
}
export default App;

Если вы посмотрите на Cypress runner, вы, вероятно, увидите что-то вроде этого:

Внутри"

Мы перейдем от «Внешнего» к «Внутреннему» и обязательно протестируем разработку нашего нового компонента.

Помните список TODO, который мы написали несколько минут назад? Давайте приступим к работе и напишем спецификацию для <EmployeeList>-компонента, который мы хотим построить.

Мы создали наше приложение, используя шаблон create-react-app, так что jest у нас уже установлено. Мы улучшим его с помощью enzyme - тестовой утилиты, которая упрощает тестирование результатов работы компонентов React. Нам нужно будет установить enzyme вместе с адаптером, соответствующим используемой нами версии React (в данном случае React 16.8.6). Мы также добавим файл конфигурации для enzyme

$ yarn add --dev enzyme enzyme-adapter-react-16
$ touch src/setupTests.js

Добавьте следующую конфигурацию в setupTests.js:

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

Нам нужно сделать еще немного дополнительную настройку ... нам нужно создать папку, в которой мы будем проводить модульные тесты. По соглашению мы будем называть его __tests__ (да, это два символа подчеркивания). Я предполагаю, что нотация с двойным подчеркиванием была выбрана случайным образом, чтобы уменьшить вероятность коллизий и указать, что она содержит только (шутливые) тесты, а не фактический производственный код. Кто знает ... Пойдем дальше ...

$ mkdir src/__tests__

Спецификация EmployeeList

Мы могли бы начать со следующей спецификации в качестве отправной точки:

import React from 'react';
import { shallow, mount } from 'enzyme';
import EmployeeList from '../components/EmployeeList'
import axios from 'axios';
describe('<EmployeeList />', () => {
  it('should fetch employees from back-end using Axios', () => {
    const axiosSpy = jest.spyOn(axios, 'get');
    shallow(
      <EmployeeList />
    )
    expect(axiosSpy).toBeCalled();
  })
  it('should render a list of 5 employees', async () => {
    const employees = {"data":[
      { "id": 1, "first_name": "George", "last_name": "Bluth", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/calebogden/128.jpg" },
      { "id": 2, "first_name": "Janet", "last_name": "Weaver", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg" },
      { "id": 3, "first_name": "Emma", "last_name": "Wong", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/olegpogodaev/128.jpg" },
      { "id": 4, "first_name": "Eve", "last_name": "Holt", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg" },
      { "id": 5, "first_name": "Charles", "last_name": "Morris", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/stephenmoon/128.jpg" }
    ]}
    // We mount the component
    const describedComponent = mount(<EmployeeList />)
    // we make sure that the components state is updated
    await describedComponent.setState({ employees: employees.data })
    // we make sure that the component renders 5 instances on a list item (<li>)
    expect(describedComponent.find('li')).toHaveLength(5);
  })
})

Эти спецификации помогут нам:

  1. убедитесь, что мы используем axios для выполнения get запроса при использовании компонента, и
  2. что после обновления состояния списком employees компонент повторно визуализируется с правильным содержимым.

Нам нужно запустить тесты и разобраться с сообщениями об ошибках. Если мы внимательно их прочитаем, мы будем указывать в правильном направлении.

Это упражнение по чтению результатов теста, реализации правильного кода и продвижению по шагам. Ты можешь это сделать!

В конце туннеля ваш EmployeeList может выглядеть примерно так:

import React, { Component } from 'react';
import axios from 'axios'
class EmployeeList extends Component {
  state = {
    employees: []
  }
  componentDidMount() {
    this.fetchEmployees()
  }
  async fetchEmployees() {
    let employees = await axios.get('https://reqres.in/api/users?per_page=5')
    this.setState({ employees: employees.data.data }) 
    // ?!? "data.data" - really?
    // Well, check out the response by setting a breakpoint just 
    // before this line withe `debugger`
  }
  render() {
    let employeeList
    employeeList = this.state.employees.map(employee => {
      return (
        <li key={employee.id}>
          {`${employee.first_name} ${employee.last_name}`}
        </li>
      )
    })
    return (
      <>
        <ul>
          {employeeList}
        </ul>
      </>
    );
  }
}
export default EmployeeList;

Интересно обратить внимание на то, что происходит с приемочным тестом. После прохождения модульного теста вам следует переключить свое внимание на бегун Cypress и проверить его статус. В прошлый раз, когда мы проверили, ошибка была довольно обширной, поскольку мы пытались использовать компонент <EmployeeList> еще до его создания. Теперь, когда он установлен, мы можем надеяться на то, что приемочные испытания обозначены зеленым цветом.

Подход приемки - модульное тестирование прошел полный цикл.

Добавить стиль

Мы довольно быстро добавим стили в список сотрудников и воспользуемся библиотекой Semantic UI React. (на мой взгляд фантастический инструмент). Настройка выполняется быстро и просто.

Шаг 1 - Установите зависимости:

$ yarn add semantic-ui-react semantic-ui-css

Шаг 2. После завершения установки импортируйте CSS в свою точку входа (index.js)

import React from 'react';
import ReactDOM from 'react-dom';
import 'semantic-ui-css/semantic.min.css';
import App from './App';
// rest of the code...

Шаг 3 - Готово! Начните использовать компоненты. Например, давайте обернем всю страницу Container, чтобы получить поля. Импортируйте его в компонент App и обновите метод render():

import React, { Component } from 'react';
import EmployeeList from './components/EmployeeList'
import { Container } from 'semantic-ui-react'

class App extends Component {
  render() {
    return (
      <>
        <Container>
          <section name="header">
            <h1>Employee list</h1>
          </section>
          <section name="main">
            <EmployeeList />
          </section>
        </Container>
      </>
    );
  }
}
export default App;

Хорошо, теперь ты знаешь. Продолжайте экспериментировать с дизайном и стилем.

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

Рассмотрим имеющийся у нас лоу-фай. В списке сотрудников мы должны отображать круглый аватар, ФИО и адрес электронной почты. В этом нам может помочь Semantic UI.

Мы можем внести следующие изменения в компонент EmployeeList:

import React, { Component } from 'react';
import { List, Image } from 'semantic-ui-react'
import axios from 'axios'
class EmployeeList extends Component {
  state = {
    employees: []
  }
  componentDidMount() {
    this.fetchEmployees()
  }
  async fetchEmployees() {
    let employees = await axios.get('https://reqres.in/api/users?per_page=5')
    this.setState({ employees: employees.data.data })
  }
  render() {
    let employeeList
    if (this.state.employees) {
      employeeList = this.state.employees.map(employee => {
        return (
          <List.Item key={employee.id}>
            <Image avatar src={employee.avatar}  />
            <List.Content>
              <List.Header as='p'>{`${employee.first_name} ${employee.last_name}`}</List.Header>
              <List.Description>
                {`${employee.first_name}@email.com`}
              </List.Description>
            </List.Content>
          </List.Item>
        )
      })
    }
    return (
      <>
        <List>
          {employeeList}
        </List>
      </>
    );
  }
}
export default EmployeeList;

Однако это изменение нарушит наши тесты!

Мы больше не отображаем <li> элементов, и поэтому мы не можем рассчитывать на то, что наши утверждения оценят истину, а наши тесты станут зелеными. Если подумать, мы проверяем эти <li> элементы как при приемке, так и в модульных тестах.

Если вы не знакомы с тем, как использовать инструменты разработчика в своем браузере и как проверять элементы DOM, это может быть довольно сложно понять.

Решение на самом деле довольно простое. Нам нужно изменить ('li') на ('div[role="listitem"]') - в части утверждения (expect()) нашего теста Cypress, а также в тесте компонентов. Вы можете понять, почему?

Заворачивать

Мы закончили первую итерацию цикла приемочно-модульного тестирования и готовы выпустить первую функцию. Следующим шагом будет взглянуть на наш lo-fi и сформулировать эту пользовательскую историю для нажатия кнопки «Просмотр» и просмотра всплывающего окна с подробностями о сотруднике.

Но это будет задача, к которой мы попросим вас погрузиться вместе со своим партнером по спариванию. Или, если у меня будет время и силы (или по многочисленным просьбам 🤪) - добавлю вторую часть этого поста…

Удачного тестирования!

/ Томас
Основатель и главный тренер
Craft Academy

Craft Academy - это провайдер технического образования, целью которого является вывод на рынок новых талантов и помощь в решении нехватки технических работников. Мы основаны на убеждении, что современные стандарты разработки, гибкие методологии и бизнес-навыки имеют основополагающее значение для профессионалов ИТЦ.

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

- 12-недельный Full Stack Developer с Rails и React Bootcamp

- или 6-недельный интерфейс разработчика с React Bootcamp.

Благодаря этому наши учащиеся находят работу в различных отраслях или открывают собственное дело, которое выводит на рынок новые инновации.

Хотите узнать больше о том, чем мы занимаемся? Подпишитесь на нас в Medium, Facebook, Twitter или посетите наш сайт.