Эта статья написана гостем Джеком Франклином . Гостевые посты SitePoint нацелены на привлечение интересного контента от известных авторов и спикеров сообщества JavaScript.
В этой статье мы рассмотрим использование Jest — платформы тестирования, поддерживаемой Facebook, — для тестирования наших компонентов ReactJS . Сначала мы рассмотрим, как мы можем использовать Jest для простых функций JavaScript, а затем рассмотрим некоторые функции, которые он предоставляет из коробки, специально предназначенные для упрощения тестирования приложений React. Стоит отметить, что Jest не предназначен специально для React: вы можете использовать его для тестирования любых приложений JavaScript. Однако некоторые функции, которые он предоставляет, очень удобны для тестирования пользовательских интерфейсов, поэтому он отлично подходит для React.
Образец заявки
Прежде чем мы сможем что-либо проверить, нам нужно приложение для тестирования! Оставаясь верным традиции веб-разработки, я создал небольшое приложение, которое мы будем использовать в качестве отправной точки. Вы можете найти его вместе со всеми тестами, которые мы собираемся написать, на GitHub . Если вы хотите поиграть с приложением, чтобы почувствовать его, вы также можете найти живую демоверсию онлайн .
Приложение написано на ES2015, скомпилировано с использованием Webpack с предустановками Babel ES2015 и React. Я не буду вдаваться в подробности настройки сборки, но все это в репозитории GitHub, если вы хотите проверить это. В README вы найдете подробные инструкции о том, как запустить приложение локально. Если вы хотите узнать больше, приложение построено с использованием Webpack , и я рекомендую « Руководство для начинающих по Webpack » как хорошее введение в инструмент.
Точкой входа приложения является app/index.js
, который просто отображает компонент Todos
в HTML:
render( <Todos />, document.getElementById('app') );
Компонент Todos
является основным центром приложения. Он содержит все состояние (жестко закодированные данные для этого приложения, которые в действительности, вероятно, поступили бы из API или аналогичного) и содержит код для визуализации двух дочерних компонентов: Todo
, который отображается один раз для каждой задачи в состоянии, и AddTodo
, который отображается один раз и предоставляет пользователю форму для добавления нового todo.
Поскольку компонент Todos
содержит все состояние, он нуждается в компонентах Todo
и AddTodo
чтобы уведомлять его о любых изменениях. Следовательно, он передает функции в эти компоненты, которые они могут вызывать при изменении некоторых данных, и Todos
может соответствующим образом обновлять состояние.
Наконец, сейчас вы заметите, что вся бизнес-логика содержится в app/state-functions.js
:
export function toggleDone(state, id) {…} export function addTodo(state, todo) {…} export function deleteTodo(state, id) {…}
Это все чистые функции, которые принимают состояние и некоторые данные и возвращают новое состояние. Если вы не знакомы с чистыми функциями, это функции, которые ссылаются только на данные, которые им дают, и не имеют побочных эффектов. Для получения дополнительной информации вы можете прочитать мою статью о чистых функциях A List Apart и мою статью о SitePoint о чистых функциях и React .
Если вы знакомы с Redux, они довольно похожи на то, что Redux назвал бы редуктором. Фактически, если бы это приложение стало намного больше, я бы подумал о переходе на Redux для более четкого, структурированного подхода к данным. Но для этого приложения размера вы часто обнаружите, что локального состояния компонента и некоторых хорошо абстрагированных функций более чем достаточно.
Для TDD или не для TDD?
Было написано много статей о плюсах и минусах разработки, основанной на тестировании , где разработчики должны сначала написать тесты, прежде чем писать код для исправления теста. Идея, лежащая в основе этого, заключается в том, что, написав тест сначала, вы должны подумать об API, который вы пишете, и это может привести к лучшему дизайну. Что касается меня, я считаю, что это очень сильно зависит от личных предпочтений, а также от того, что я тестирую. Я обнаружил, что для компонентов React мне нравится сначала писать компоненты, а затем добавлять тесты к наиболее важным элементам функциональности. Однако, если вы обнаружите, что написание тестов в первую очередь для ваших компонентов соответствует вашему рабочему процессу, вам следует это сделать. Здесь нет жесткого правила; делай все, что захочешь для себя и своей команды.
Обратите внимание, что эта статья будет посвящена тестированию внешнего кода. Если вы ищете что-то сфокусированное на серверной части, обязательно ознакомьтесь с курсом Site- Test-Driven Development в Node.js.
Представляя Jest
Впервые Jest был выпущен в 2014 году, и хотя изначально он вызвал большой интерес, проект некоторое время находился в бездействии и не так активно работал над ним. Тем не менее, в прошлом году Facebook вложил средства в улучшение Jest и недавно опубликовал несколько выпусков с впечатляющими изменениями, которые стоит пересмотреть. Единственное сходство Jest по сравнению с первоначальным релизом с открытым исходным кодом — это название и логотип. Все остальное было изменено и переписано. Если вы хотите узнать больше об этом, вы можете прочитать комментарий Кристофа Пойера , где он обсуждает текущее состояние проекта.
Если вы расстроены настройкой тестов Babel, React и JSX с использованием другого фреймворка, то я определенно рекомендую попробовать Jest. Если вы обнаружили, что ваша существующая настройка теста работает медленно, я также настоятельно рекомендую Jest. Он автоматически запускает тесты параллельно, а его режим просмотра может запускать только тесты, относящиеся к измененному файлу, что неоценимо при наличии большого набора тестов. Он поставляется с настроенным JSDom , то есть вы можете писать тесты для браузера, но запускать их через Node, работать с асинхронными тестами и иметь расширенные функции, такие как насмешки, шпионы и заглушки.
Установка и настройка Jest
Для начала нам нужно установить Jest. Поскольку мы также используем Babel, мы установим еще пару модулей, которые позволят Jest и Babel хорошо играть из коробки:
npm install --save-dev babel-jest babel-polyfill babel-preset-es2015 babel-preset-react jest
Вам также необходимо иметь файл .babelrc
с Babel, настроенным для использования любых необходимых пресетов и плагинов. В примере проекта уже есть этот файл, который выглядит так:
{ "presets": ["es2015", "react"] }
Мы пока не будем устанавливать какие-либо инструменты тестирования React, потому что мы не собираемся начинать с тестирования наших компонентов, а наших функций состояния.
Jest ожидает найти наши тесты в папке __tests__
, которая стала популярным соглашением в сообществе JavaScript, и мы собираемся придерживаться его. Если вы не являетесь поклонником установки __tests__
, Jest также поддерживает поиск любых .test.js
и .spec.js
.
Поскольку мы будем тестировать наши функции состояния, __tests__/state-functions.test.js
создадим __tests__/state-functions.test.js
.
Мы напишем правильный тест в ближайшее время, но пока, включите этот фиктивный тест, который позволит нам проверить, что все работает правильно, и у нас настроен Jest.
describe('Addition', () => { it('knows that 2 and 2 make 4', () => { expect(2 + 2).toBe(4); }); });
Теперь package.json
в ваш package.json
. Нам нужно настроить npm test
чтобы он запускал Jest, и мы можем сделать это, просто установив test
скрипт для запуска jest
.
"scripts": { "test": "jest" }
Если вы теперь запускаете npm test
локально, вы должны увидеть, как ваши тесты выполняются, и пройти!
PASS __tests__/state-functions.test.js Addition ✓ knows that 2 and 2 make 4 (5ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 passed, 0 total Time: 3.11s
Если вы когда-либо пользовались Jasmine или большинством фреймворков для тестирования, сам код теста выше должен быть довольно знакомым. Jest позволяет нам использовать describe
и вставлять тесты так, как нам нужно. Сколько вложений вы используете, зависит от вас; Мне нравится вкладывать мои так, чтобы все описательные строки были переданы для describe
и it
читается почти как предложение.
Когда дело доходит до создания фактических утверждений, вы оборачиваете объект, который вы хотите проверить, в вызов expect()
, прежде чем вызывать утверждение для него. В этом случае мы привыкли toBe
. Вы можете найти список всех доступных утверждений в документации Jest . toBe
проверяет соответствие заданного значения тестируемому значению, используя для этого ===
. Мы встретимся с некоторыми утверждениями Джеста в этом уроке.
Тестирование бизнес-логики
Теперь мы видели, как Jest работает над фиктивным тестом, давайте запустим реальный тест! Мы собираемся протестировать первую из наших функций состояния, toggleDone
. toggleDone
принимает текущее состояние и идентификатор задачи, которую мы хотели бы переключить. У каждой задачи есть свойство done
, и toggleDone
должен поменять его с true
на false
или наоборот.
Если вы придерживаетесь этого, убедитесь, что вы клонировали репо и скопировали папку app
в тот же каталог, в котором находится папка ___tests__
. Вам также необходимо установить пакет shortid
( npm install shortid --save
), который является зависимостью от приложения Todo.
Я начну с импорта функции из app/state-functions.js
и настройки структуры теста. В то время как Jest позволяет вам использовать describe
и вкладывать его так глубоко, как вы хотите, вы также можете использовать test
, который часто будет читать лучше. test
— это просто псевдоним функции Jest’s it
, но иногда он делает тесты намного проще для чтения и менее вложенными.
Например, вот как я написал бы этот тест с вложенным describe
и it
вызывает:
import { toggleDone } from '../app/state-functions'; describe('toggleDone', () => { describe('when given an incomplete todo', () => { it('marks the todo as completed', () => { }); }); });
А вот как я бы сделал это с помощью test
:
import { toggleDone } from '../app/state-functions'; test('toggleDone completes an incomplete todo', () => { });
Тест все еще хорошо читается, но теперь мешает меньше отступов. Этот в основном зависит от личных предпочтений; выберите тот стиль, который вам удобнее.
Теперь мы можем написать утверждение. Сначала мы создадим наше начальное состояние, прежде чем передать его в toggleDone
вместе с идентификатором задачи, которую мы хотим переключить. toggleDone
вернет наше состояние завершения, которое мы затем можем подтвердить:
const startState = { todos: [{ id: 1, done: false, name: 'Buy Milk' }] }; const finState = toggleDone(startState, 1); expect(finState.todos).toEqual([ { id: 1, done: true, name: 'Buy Milk' } ]);
Теперь обратите внимание, что я использую toEqual
чтобы сделать свое утверждение. Вы должны использовать toBe
для примитивных значений, таких как строки и числа, но toEqual
для объектов и массивов. toEqual
создан для работы с массивами и объектами и будет рекурсивно проверять каждое поле или элемент в данном объекте, чтобы убедиться, что он совпадает.
Теперь мы можем запустить npm test
и посмотреть, как прошел тест функции состояния:
PASS __tests__/state-functions.test.js ✓ tooggleDone completes an incomplete todo (9ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 passed, 0 total Time: 3.166s
Повторные тесты на изменения
Немного неприятно вносить изменения в тестовый файл, а затем снова запускать npm test
вручную. Одна из лучших функций Jest — это режим просмотра, который отслеживает изменения файлов и соответственно запускает тесты. Он даже может выяснить, какое подмножество тестов нужно запустить на основе файла, который изменился. Он невероятно мощный и надежный, и вы можете запускать Jest в режиме просмотра и оставлять его на весь день, пока создаете свой код.
Чтобы запустить его в режиме просмотра, вы можете запустить npm test -- --watch
. Все, что вы передадите в npm test
после первого --
будет напрямую передано основной команде. Это означает, что эти две команды фактически эквивалентны:
-
npm test -- --watch
-
jest --watch
Я бы порекомендовал оставить Jest запущенным в другой вкладке или окне терминала для оставшейся части этого урока.
Прежде чем перейти к тестированию компонентов React, мы напишем еще один тест для другой из наших функций состояния. В реальном приложении я написал бы еще много тестов, но для обучения я пропущу некоторые из них. А сейчас давайте напишем тест, который гарантирует, что наша функция deleteTodo
работает. Прежде чем посмотреть, как я написал это ниже, попробуйте написать это самостоятельно и посмотреть, как ваш тест сравнивается.
Помните, что вам придется обновить оператор import
вверху, чтобы импортировать deleteTodo
вместе с toggleTodo
:
import { toggleTodo, deleteTodo } from '../app/state-functions';
И вот как я написал тест:
test('deleteTodo deletes the todo it is given', () => { const startState = { todos: [{ id: 1, done: false, name: 'Buy Milk' }] }; const finState = deleteTodo(startState, 1); expect(finState.todos).toEqual([]); });
Тест не слишком сильно отличается от первого: мы устанавливаем наше начальное состояние, запускаем нашу функцию, а затем утверждаем конечное состояние. Если вы оставили Jest запущенным в режиме наблюдения, обратите внимание, как он подхватывает ваш новый тест и запускает его, и как быстро он это делает! Это отличный способ получить мгновенный отзыв о ваших тестах по мере их написания.
Вышеприведенные тесты также демонстрируют идеальный макет для теста, который:
- настроить
- выполнить тестируемую функцию
- утверждать на результатах.
Придерживаясь тестов, изложенных таким образом, вам будет легче следить за ними и работать с ними.
Теперь мы довольны тестированием наших функций состояния, давайте перейдем к компонентам React.
Тестирование компонентов React
Стоит отметить, что по умолчанию я бы рекомендовал вам не писать слишком много тестов для ваших компонентов React. Все, что вы хотите очень тщательно протестировать, например, бизнес-логику, должно быть извлечено из ваших компонентов и находиться в автономных функциях, подобно функциям состояния, которые мы тестировали ранее. Тем не менее, иногда полезно протестировать некоторые взаимодействия React (например, убедиться, что определенная функция вызывается с правильными аргументами, когда пользователь нажимает кнопку). Мы начнем с тестирования того, что наши компоненты React отображают правильные данные, а затем рассмотрим тестирование взаимодействия. Затем мы перейдем к снимкам, особенность Jest, которая делает тестирование вывода компонентов React намного более удобным.
Чтобы сделать это, нам нужно будет использовать react-addons-test-utils
, библиотеку, которая предоставляет функции для тестирования React . Мы также установим Enzyme , библиотеку-оболочку, написанную AirBnB, которая значительно упрощает тестирование компонентов React. Мы будем использовать этот API на протяжении наших тестов. Enzyme — фантастическая библиотека, и команда React даже рекомендует ее как способ тестирования компонентов React.
npm install --save-dev react-addons-test-utils enzyme
Давайте проверим, что компонент Todo
отображает текст его задачи в абзаце. Сначала мы создадим __tests__/todo.test.js
и импортируем наш компонент:
import Todo from '../app/todo'; import React from 'react'; import { mount } from 'enzyme'; test('Todo component renders the text of the todo', () => { });
Я также импортирую mount
из фермента. Функция mount
используется для визуализации нашего компонента, а затем позволяет нам проверять вывод и делать на нем утверждения. Несмотря на то, что мы запускаем наши тесты в Node, мы все равно можем писать тесты, которые требуют DOM. Это потому, что Jest настраивает jsdom , библиотеку, которая реализует DOM в Node. Это здорово, потому что мы можем писать тесты на основе DOM без необходимости каждый раз запускать браузер для их тестирования.
Мы можем использовать mount
для создания нашего Todo
:
const todo = { id: 1, done: false, name: 'Buy Milk' }; const wrapper = mount( <Todo todo={todo} /> );
И тогда мы можем вызвать wrapper.find
, предоставив ему CSS-селектор, чтобы найти абзац, который, как мы ожидаем, будет содержать текст Todo. Этот API может напоминать вам о jQuery, и это специально. Это очень интуитивно понятный API для поиска отрендеренного вывода, чтобы найти соответствующие элементы.
const p = wrapper.find('.toggle-todo');
И наконец, мы можем утверждать, что текст в нем — « Buy Milk
:
expect(p.text()).toBe('Buy Milk');
Что оставляет наш весь тест похожим на это:
import Todo from '../app/todo'; import React from 'react'; import { mount } from 'enzyme'; test('TodoComponent renders the text inside it', () => { const todo = { id: 1, done: false, name: 'Buy Milk' }; const wrapper = mount( <Todo todo={todo} /> ); const p = wrapper.find('.toggle-todo'); expect(p.text()).toBe('Buy Milk'); });
Уф! Вы можете подумать, что было много работы и усилий, чтобы проверить, что «Buy Milk» помещается на экран, и, ну… вы были бы правы. Но пока держите лошадей; в следующем разделе мы рассмотрим использование возможности снимка Jest, чтобы сделать это намного проще.
А пока давайте посмотрим, как вы можете использовать шпионские функции Jest, чтобы утверждать, что функции вызываются с конкретными аргументами. Это полезно в нашем случае, потому что у нас есть компонент Todo
которому даны две функции как свойства, которые он должен вызывать, когда пользователь нажимает кнопку или выполняет взаимодействие.
В этом тесте мы собираемся утверждать, что при щелчке по todo, компонент будет вызывать doneChange
которое он дал.
test('Todo calls doneChange when todo is clicked', () => { });
То, что мы хотим сделать, это иметь функцию, которую мы можем отслеживать ее вызовы и аргументы, с которыми она вызывается. Затем мы можем проверить, что когда пользователь щелкает doneChange
, doneChange
функция doneChange
а также вызывается с правильными аргументами. К счастью, Jest предоставляет это из коробки со шпионами. Шпион — это функция, реализация которой вас не волнует; Вы просто заботитесь о том, когда и как это называется. Думайте об этом, как будто вы следите за функцией. Чтобы создать его, мы вызываем jest.fn()
:
const doneChange = jest.fn();
Это дает функцию, за которой мы можем следить и убедиться, что она вызывается правильно. Давайте начнем с рендеринга нашего Todo
с правильными опорами:
const todo = { id: 1, done: false, name: 'Buy Milk' }; const doneChange = jest.fn(); const wrapper = mount( <Todo todo={todo} doneChange={doneChange} /> );
Далее мы можем снова найти наш абзац, как в предыдущем тесте:
const p = TestUtils.findRenderedDOMComponentWithClass(rendered, 'toggle-todo');
И тогда мы можем вызвать simulate
на нем для имитации пользовательского события, передавая click
в качестве аргумента:
p.simulate('click');
И все, что осталось сделать, это утверждать, что наша шпионская функция была вызвана правильно. В этом случае мы ожидаем, что он будет вызван с идентификатором задачи, который равен 1
. Мы можем использовать expect(doneChange).toBeCalledWith(1)
чтобы подтвердить это, и с этим мы закончили наш тест!
test('TodoComponent calls doneChange when todo is clicked', () => { const todo = { id: 1, done: false, name: 'Buy Milk' }; const doneChange = jest.fn(); const wrapper = mount( <Todo todo={todo} doneChange={doneChange} /> ); const p = wrapper.find('.toggle-todo'); p.simulate('click'); expect(doneChange).toBeCalledWith(1); });
Лучшее тестирование компонентов со снимками
Я упоминал выше, что для тестирования компонентов React может потребоваться большая работа, особенно некоторые из более обыденных функций (например, визуализация текста). Вместо того чтобы делать большое количество утверждений для компонентов React, Jest позволяет запускать тесты моментальных снимков. Они не очень полезны для взаимодействий (в этом случае я все же предпочитаю тест, как мы только что написали выше), но для проверки правильности вывода вашего компонента они намного проще.
Когда вы запускаете тест снимка, Jest отображает тестируемый компонент React и сохраняет результат в файле JSON. Каждый раз, когда запускается тест, Jest будет проверять, что компонент React по-прежнему выводит тот же вывод, что и снимок. Затем, когда вы измените поведение компонента, Jest сообщит вам, а также:
- вы поймете, что сделали ошибку, и вы можете исправить компонент так, чтобы он снова соответствовал снимку
- или, вы сделали это изменение специально, и вы можете сказать Jest обновить снимок.
Этот способ тестирования означает, что:
- вам не нужно писать много утверждений, чтобы гарантировать, что ваши компоненты React работают должным образом
- Вы никогда не сможете случайно изменить поведение компонента, потому что Jest поймет.
Вам также не нужно снимать все ваши компоненты. На самом деле, я бы активно рекомендовал против этого. Вы должны выбрать компоненты с некоторыми функциональными возможностями, которые вам действительно нужны для обеспечения работы. Снимок всех ваших компонентов приведет к медленным тестам, которые не будут полезны. Помните, что React — это очень тщательно протестированная среда, поэтому мы можем быть уверены, что она будет работать так, как ожидается. Убедитесь, что вы в конечном итоге не тестируете фреймворк, а не свой код!
Чтобы начать тестирование снимков, нам нужен еще один пакет Node. response-test-renderer — это пакет, который может взять компонент React и отобразить его как чистый объект JavaScript. Это означает, что он может быть сохранен в файл, и это то, что Jest использует для отслеживания наших снимков.
npm install --save-dev react-test-renderer
Теперь давайте перепишем наш первый тест компонента Todo, чтобы использовать снимок. На данный момент, закомментируйте TodoComponent calls doneChange when todo is clicked
test.
Первое, что вам нужно сделать, это импортировать react-test-renderer
, а также удалить импорт для mount
. Они не могут быть оба использованы; Вы должны либо использовать один или другой. Вот почему мы прокомментировали другой тест на данный момент.
import renderer from 'react-test-renderer';
Теперь я буду использовать средство визуализации, которое мы только что импортировали, для визуализации компонента и утверждаю, что он соответствует снимку:
describe('Todo component renders the todo correctly', () => { it('renders correctly', () => { const todo = { id: 1, done: false, name: 'Buy Milk' }; const rendered = renderer.create( <Todo todo={todo} /> ); expect(rendered.toJSON()).toMatchSnapshot(); }); });
При первом запуске Jest достаточно умен, чтобы понять, что для этого компонента нет снимка, поэтому он его создает. Давайте посмотрим на __tests__/__snapshots__/todo.test.js.snap
:
exports[`Todo component renders the todo correctly renders correctly 1`] = ` <div className="todo todo-1"> <p className="toggle-todo" onClick={[Function]}> Buy Milk </p> <a className="delete-todo" href="#" onClick={[Function]}> Delete </a> </div> `;
Вы можете видеть, что Jest сохранил вывод для нас, и теперь, когда мы в следующий раз запустим этот тест, он проверит, что выходы совпадают. Чтобы продемонстрировать это, я разбью компонент, удалив абзац, который отображает текст задачи, что означает, что я удалил эту строку из компонента Todo
:
<p className="toggle-todo" onClick={() => this.toggleDone() }>{ todo.name }</p>
Давайте посмотрим, что сейчас говорит Джест:
FAIL __tests__/todo.test.js ● Todo component renders the todo correctly › renders correctly expect(value).toMatchSnapshot() Received value does not match stored snapshot 1. - Snapshot + Received <div className="todo todo-1"> - <p - className="toggle-todo" - onClick={[Function]}> - Buy Milk - </p> <a className="delete-todo" href="#" onClick={[Function]}> Delete </a> </div> at Object.<anonymous> (__tests__/todo.test.js:21:31) at process._tickCallback (internal/process/next_tick.js:103:7)
Джест понял, что снимок не соответствует новому компоненту, и дал нам знать в выводе. Если мы считаем это изменение правильным, мы можем запустить jest с флагом -u
, который обновит снимок. В этом случае, однако, я отменю свои изменения, и Джест снова счастлив.
Далее мы можем посмотреть, как мы можем использовать тестирование снимков для тестирования взаимодействий. Вы можете иметь несколько снимков для каждого теста, поэтому вы можете проверить, что результат после взаимодействия соответствует ожидаемому.
На самом деле мы не можем проверить взаимодействие наших компонентов Todo с помощью снимков Jest, потому что они не контролируют свое собственное состояние, а вызывают реквизиты обратного вызова, которые им дают. Здесь я переместил тест снимка в новый файл todo.snapshot.test.js и оставил наш переключающий тест в todo.test.js. Я нашел полезным разделить тесты снимков в другой файл; это также означает, что вы не получите конфликтов между react-test-renderer
и react-addons-test-utils
.
Помните, что вы найдете весь код, который я написал в этом руководстве, на GitHub, чтобы вы могли проверить его и запустить локально.
Вывод
Facebook выпустил Jest давным-давно, но в последнее время его взяли и над ним работали чрезмерно. Это быстро стало фаворитом для разработчиков JavaScript, и будет только лучше. Если вы пробовали Jest в прошлом и вам это не понравилось, я не могу вас подбодрить, чтобы попробовать еще раз, потому что сейчас это практически другая структура. Он быстр, отлично справляется с перезапуском спецификаций, выдает фантастические сообщения об ошибках и дополняет их всеми функциями снимков.
Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь поднимать вопрос на GitHub, и я буду рад помочь. И, пожалуйста, не забудьте проверить Jest на GitHub и начать проект; это помогает сопровождающим.
Эта статья была рецензирована Дэном Принсом и Кристофом Пожером . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!