Моки — JS: Продвинутое тестирование
В тестировании очень популярен мокинг. Технически он похож на стабинг, и из-за этого их часто путают (специально или ненамеренно). Но все же они служат разным целям и используются в разных ситуациях. Разберемся, что это такое, и когда он нужен.
До этого момента мы рассматривали побочные эффекты как помеху тестирования нашей логики. Для их изоляции использовались стабы или прямое выключение логики в тестовой среде. После этого можно было спокойно проверять правильность работы функции.
В некоторых ситуациях требуется кое-что другое. Не результат работы функции, а то, что она выполняет нужное нам действие, например, шлет правильный HTTP-запрос с правильными параметрами. Для этого понадобятся моки. Моки проверяют, как выполняется код.
HTTP
import nock from 'nock'; import getPrivateForkNames > from '../src.js'; // Предотвращение случайных запросов nock.disableNetConnect(); test('getPrivateForkNames', async () => // Полное название домена const scope = nock('https://api.github.com') // Полный путь .get('/orgs/hexlet/repos/?private=true') .reply(200, [ fork: true, name: 'one' >, fork: false, name: 'two' >]); await getPrivateForkNames('hexlet'); // Метод `scope.isDone()` возвращает `true` только тогда, // когда соответствующий запрос был выполнен внутри `getPrivateForkNames` expect(scope.isDone()).toBe(true); >);
Это и называется мокинг. Мок проверяет, что какой-то код выполнился определенным образом. Это может быть вызов функции, HTTP-запрос и тому подобное. Задача мока убедиться в том, что это произошло, и в том, как конкретно это произошло, например, что в функцию были переданы конкретные данные.
Что дает нам такая проверка? В данном случае — мало что. Да, мы убеждаемся, что вызов был, но само по себе это еще ни о чем не говорит. Так когда же полезны моки?
Представьте, что мы бы разрабатывали библиотеку @octokit/rest, ту самую, что выполняет запросы к GitHub API. Вся суть этой библиотеки в том, чтобы выполнить правильные запросы с правильными параметрами. Поэтому там нужно обязательно проверять выполнение запросов с указанием точных URL-адресов. Только в таком случае можно быть уверенными, что она выполняет верные запросы.
В этом ключевое отличие мока от стаба. Стаб устраняет побочный эффект, чтобы не мешать проверке результата работы кода, например, возврату данных из функции. Мок фокусируется на том, как конкретно работает код, что он делает внутри. При этом чисто технически мок и стаб создаются одинаково, за исключением того, что на мок вешают ожидания, проверяющие вызовы. Это приводит к путанице, потому что часто моками называют стабы. С этим ничего уже не поделать, но для себя всегда пытайтесь понять, о чем идет речь. Это важно, от этого зависит фокус тестов.
Функции
Моки довольно часто используют с функциями (методами). К примеру, они могут проверять:
- Что функция была вызвана
- Сколько раз она была вызвана
- Какие аргументы мы использовали
- Сколько аргументов было передано в функцию
- Что вернула функция
Предположим, что мы хотим протестировать функцию forEach . Она вызывает колбек для каждого элемента коллекции:
[1, 2, 3].forEach((v) => console.log(v)); // или проще [1, 2, 3].forEach(console.log)
Эта функция ничего не возвращает, поэтому напрямую ее не протестировать. Можно попробовать сделать это с помощью моков. Проверим, что она вызывает переданный колбек и передает туда нужные значения.
Так как мы изучаем Jest, то для создания моков воспользуемся встроенным механизмом Jest. В других фреймворках могут быть свои встроенные механизмы. Кроме того, как мы убедились выше, существуют специализированные библиотеки для моков и стабов.
test('forEach', () => // Моки функций в Jest создаются с помощью функции jest.fn // Она возвращает функцию, которая запоминает все свои вызовы и переданные аргументы // Потом это используется для проверок const callback = jest.fn(); [1, 2, 3].forEach(callback); // Теперь проверяем, что она была вызвана с правильными аргументами нужное количество раз expect(callback.mock.calls).toHaveLength(3); // Первый аргумент первого вызова expect(callback.mock.calls[0][0]).toBe(1); // Первый аргумент второго вызова expect(callback.mock.calls[1][0]).toBe(2); // Первый аргумент третьего вызова expect(callback.mock.calls[2][0]).toBe(3); >);
С помощью моков мы проверили, что функция была вызвана ровно три раза, и ей, последовательно для каждого вызова, передавался новый элемент коллекции. В принципе, можно сказать, что этот тест действительно проверяет работоспособность функции forEach() . Но можно ли сделать это проще, без мока и без завязки на внутреннее поведение? Оказывается, можно. Для этого достаточно использовать замыкание:
test('forEach', () => const result = []; const numbers = [1, 2, 3]; numbers.forEach((x) => result.push(x)); expect(result).toEqual(numbers); >);
Объекты
Jest позволяет создавать моки и для объектов. Они создаются с помощью функции jest.spyOn() , напоминающей уже известную нам jest.fn() . Эта функция принимает на вход объект и имя метода в этом объекте, и отслеживает вызовы этого метода. Отследим, в качестве примера, вызов console.log() :
test('logging something', () => const spy = jest.spyOn(console, 'log'); console.log(12); expect(spy).toHaveBeenCalled(); // => true, т.к. метод log был вызван expect(spy).toHaveBeenCalledTimes(1); // => true, т.к. метод был вызван 1 раз expect(spy).toHaveBeenCalledWith(12); // true, т.к. log был вызван с аргументом 12 expect(spy.mock.calls[0][0]).toBe(12); // проверка, идентичная предыдущей >);
Кроме того, Jest позволяет создавать свою реализацию сбора данных о вызовах отслеживаемой функции при помощи метода mockImplementation(fn) . Колбэком этого метода будет функция, которая выполняется после каждого вызова отслеживаемой функции. Возьмем предыдущий пример, но теперь соберем аргументы каждого вызова console.log() в отдельный массив:
test('logging something', () => const logCalls = []; // Здесь функция внутри mockImplementation принимает на вход аргумент(ы), // с которым был вызван console.log, и сохраняет его в заранее созданный массив const spy = jest.spyOn(console, 'log').mockImplementation((. args) => logCalls.push(args)); console.log('one'); console.log('two'); expect(logCalls.join(' ')).toBe('one two'); >);
Преимущества и недостатки
Несмотря на то, что существуют ситуации, в которых моки нужны, в большинстве ситуаций их нужно избегать. Моки слишком много знают о том, как работает код. Любой тест с моками из черного ящика превращается в белый ящик. Повсеместное использование моков приводит к двум вещам:
- После рефакторинга приходится переписывать тесты (много тестов!), даже если код работает правильно. Происходит это из-за завязки на то, как конкретно работает код
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Тестирование для “чайников”.
Тестирование — это процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.
Когда нужно и не нужно тестировать
Всегда ли нужно тестировать ваше приложение? Рекомендуем ответить вам самим на этот вопрос после следующего утверждения: “Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля”.
Однако существуют ситуации, когда необходимость в написании тестов не столь очевидна. Скорее всего тесты не нужны вам если:
- Вы всегда пишете код для себя, создавая pet проект, не имея планов на его дальнейшее коммерческое использование или распространение в рамках сообщества.
- Вы создаете рекламные одностраничники, продающие страницы, простые флеш-играми или баннеры — сложная верстка/анимация или большой объем статики. Никакой логики нет, только представление.
- Создание статического сайта-визитки, т.е. 1–4 html-страницы с одной или несколькими формами для отправки данных. Тут закономерно нет никакой особенной логики, быстрее просто все проверить самостоятельно, так сказать «руками».
- Вы делаете рекламный проект для выставки. Срок работы — от нескольких недель до месяца. В начале проекта не до конца известно, что именно должно получиться в конце. Задача проекта — отработать несколько дней на выставке в качестве презентации.
В представленных вариантах по объективным причинам (сжатые сроки, бюджеты, размытые цели или очень простые требования) вы не получите выигрыша от написания тестов.
Основные термины (пирамида, TDD, BDD, stubs, mock)
Существует несколько подходов к написанию тестов. Первая модель — классика: сначала разработка, а затем тестирование “code first”. Это означает, что сначала происходит написание кода, затем мы тестируем продукт и отправляем его или на доработку, или переходим к следующей стадии разработки.
Другой подход можно назвать “test first” режимом. Это означает, что мы можем начать тестирование еще до написания самой функции — например, мы можем создать единичный тест или автоматически выполняемый набор тестов до того, как функция или какой-то кусок кода будет разработан и внедрен в приложение. Одним из наиболее популярных примеров здесь является Test-Driven Development.
Тем не менее, стоит упомянуть, что техника “test first” не так популярна, как “code first”. Это связано с тем, что в большинстве проектов все еще сложно автоматизировать что то, что еще не было разработано. Обобщая оба упомянутых выше подхода, можно сделать вывод, что нет особой разницы и что автоматизацию тестов мы можем использовать в любом из вариантов. Ни один из этих подходов не может считаться хорошим или плохим и выбор в первую очередь зависит от проекта т.е. каждый конкретный случай следует рассматривать отдельно.
Также наряду с термином ТDD вы можете услышать и о BDD подходе.
В основе Test-driven development (TDD) лежит 5 основных этапов:
- Сначала разработчик пишет несколько тестов.
- Затем разработчик запускает эти тесты и (очевидно) они терпят неудачу, потому что ни одна из этих функций еще не реализована.
- Далее разработчик действительно реализует эти тесты в коде.
- Если разработчик хорошо пишет свой код, то на следующем этапе он увидит, что тесты проходят.
- Разработчик может затем реорганизовать свой код, добавить комментарии так как он уверен, что если новый код что-то сломает, тогда тесты предупредят об этом.
test('равно 1 для массива с одним элементом', function () assert.equal(1,[1].length);>);
Behavior-driven development (BDD) — подход создан для того, чтобы исправить проблемы, которые могут возникнуть при использовании ТDD, а именно, обеспечить лучшее взаимопонимание внутри команды, т.е. не только для разработчиков, облегчить поддержку кода через наглядное представление о его функциональности, тесты и их результаты выглядят более “человечно”, облегчается процесс миграции при переходе на другой язык программирования.
В варианте с BDD — в начале мы описываем поведение и спецификации, которые затем управляют нашей разработкой программного обеспечения. Поведение и спецификации могут показаться ужасно похожими на тесты, но разница очень тонкая и важная.
Это, по сути, создание плана перед тем, как вы начинаете писать код. Т.е в начале описываем, что должно происходить, каково поведение функции (в заголовках тестов пишем, не что мы проверяем, а то, что мы ожидаем от работы еще не реализованной функциональности), затем пишем тесты и наконец, сам код.
it('должно вернуть 1, когда передан массив с одним элементов', function () [1].length.should.equal(1);>);
Еще одной важной концепцией тестирования является тестовая пирамида. Пирамида тестирования используется для распределения тестов по уровням приложения.
Каждое приложение можно разделить на несколько слоев. Рассмотрим типичное расслоение с уровнем компонентов, сервисами и пользовательским интерфейсом. Нижняя часть пирамиды покрыта модульными (unit) тестами. Они написаны в основном разработчиками и охватывают атомарные компоненты, такие как классы, методы и функции. Запускаются очень часто, работают быстро и их количество в рамках приложения велико.
Б. Страустрап в своей книге о C++ предлагает следующий подход к разделению кода на отдельные блоки: если «это» действие — сделайте метод. Если несколько действий объединены общим смыслом и/или процессом — объявите класс. Если придерживаться этого правила, то автоматически класс будет модулем вашего приложения.
Следующий уровень — интеграционные тесты. Т.е. когда идет проверка, не ломает ли новый функционал код, который уже написан ранее в рамках системы. Также тут мы можем иметь сценарии, которые охватывают более сложные функции, такие как тесты API. Запускаем реже, как правило, при мердже веток или объединении больших участков кода.
В верхней части находятся тесты пользовательского интерфейса (end to end) Они действуют так же, как конечный пользователь работает с приложением. Запускаем очень редко — несколько раз за проект. Работают очень медленно.
Процесс автоматизации тестирования характеризуется движением от основания пирамиды к ее вершине. Если одно задание терпит неудачу, то мы начинаем с самого начала. Этот конвейер продолжается до тех пор, пока весь набор тестов не будет выполнен. Возможен также параллельный запуск тестов для сокращения времени, необходимого для выполнения всех заданий.
Еще один набор терминов, с которыми придется столкнуться в процессе написания тестов — это стабы (stubs) и моки (mock).
Очень часто наш код (функция, модуль) имеют внешние зависимости. Внешняя зависимость — это все, что делает ваши тесты не правдивыми и сложно-поддерживаемыми. Файловая система — зависимость: структура каталогов может быть другой на другой машине. База данных — зависимость, ее может не быть на другой машине. Веб-сервис — зависимость: может не быть интернета или может присутствовать фаервол и.т.д
Если на вопрос: «будет ли этот компонент вести себя так же на другой машине?» вы отвечаете нет, то его необходимо “подменить” и тут вам на помощь как раз придут стабы и моки. Но есть и другая сторона медали, когда разработчик начинает увлекаться и приходит к тому, что подменяет вообще все. Соответственно тесты перестают проверять само приложение и начинают тестировать стабы, моки. Это в корне не верно. Если «живых» реализаций в тесте нет, то этот тест не тестирует ничего.
Иногда эти термины stubs и mock путают: разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок — это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.
С технической точки зрения это значит, что, используя стабы, мы проверяем состояние тестируемого класса или результат выполненного метода. При использовании мока мы проверяем, соответствуют ли ожидания мока поведению тестируемого класса. Также лучше использовать не более одного мока на тест. Иначе с высокой вероятностью вы нарушите принцип «тестировать только одну вещь». При этом, в одном тесте может быть сколько угодно стабов или же мок и стабы.
Есть уже готовые фреймворки, которые предоставляют такой функционал: Sinon, Jasmine, enzyme, Jest, testdouble
Пример использования стаба (sinonjs.org)
it('resolves with the right name', done => const stub = sinon.stub(User.prototype, 'fetch') .resolves(< name: 'David' >) User.fetch() .then(user => expect(user.name).toBe('David') done() >)>)
Пишем тесты правильно (требования, оценка результата)
На сегодняшний день, к сожалению, сохраняется ситуация, когда тесты к проекту написаны, но что они тестируют, и какой от них ожидается результат, в полной мере неизвестно. Чаще всего это результат бездумного написания тестов, что не только не помогает, но вредит проекту. Тесты написанные без учета архитектуры и конкретного плана действий только осложняют сопровождение и поддержку, так как вместо одного некачественного продукта вы получаете два.
Есть несколько рекомендаций, придерживаясь которых, вы можете избежать данной проблемы.
1) Тесты в пределах проекта должны быть расположены в соответствии с общей логикой и должны быть частью системы контроля версий. Например, если приложение монолитное, положите все тесты в папку test; если у вас много разных компонентов, храните тесты в папке каждого компонента.
2) Особое внимание уделите именованию тестов. Одна из лучших практик: добавьте к каждому проекту его собственный тестовый проект.
Если у вас, например, есть части системы your-project.models, your-project.controllers, то тесты для этих частей могут именоваться следующим образом: your-project.models.tests, your-project.controllers.tests
3)Такие же “логичные” походы используйте для именования тестовых классов или методов.
Например, один из вариантов именования для тестирования метода — __ .
Метод калькулятора суммирующий данные
Sum_2Plus5_Returned7
4)Каждый тестирующий класс или метод должен тестировать только одну сущность. Если процесс слишком сложен (например, покупка в интернет-магазине), разделите его на несколько частей и протестируйте их отдельно.
5)Не делайте ненужных утверждений (assertion). Какое конкретное поведение вы тестируете? Если это не основное поведение, то оно и не нуждается в тестировании! Помните, что модульные тесты — это спецификация дизайна того, как должно срабатывать определенное поведение, а не список наблюдений за всем кодом.
6) Вы определенно ошиблись, если вам нужно запускать тесты в определенном порядке, или если они работают только при активной базе данных или сетевом соединении.
7)Не стоит писать велосипеды. Пользуйтесь готовыми тестовыми фреймворками. Подберите тот, который подходит вам в данном, конкретном случае.
8)Боритесь с зависимостями. Тесты не должны зависеть от окружения, в котором они выполняются. Например, не проверяйте настройки конфигурации устройства. По необходимости используйте стабы и моки, а также готовые фреймворки для их написания.
9)Не относитесь к своим тестам как к второсортному коду. Все принципы, применяемые в разработке продакшн-кода могут и должны применяться при написании тестов. (DRY, KISS)
10)Тест должен легко поддерживаться. Есть всего три причины, почему тест перестал проходить:
- Ошибка в продакшн-коде: это баг, его нужно завести в баг-трекере и починить.
- Баг в тесте: видимо, продакшн-код изменился, а тест написан с ошибкой (например, тестирует слишком много или не то, что было нужно).
- Смена требований. Если требования изменились слишком сильно — тест должен упасть. Это правильно и нормально. Вам нужно разобраться с новыми требованиями и исправить тест. Или удалить, если он больше не актуален.
11)Тесты должны запускаться регулярно в автоматическом режиме.
Определить успешность вашей системы тестирования на проекте можно двумя способами:
- Количество багов в новых релизах (в т.ч. и регрессии).
- Покрытие кода.
Первый показывает, есть ли у наших действий результат, или мы впустую расходуем время. Второй — как много нам еще предстоит сделать.
Тестовое покрытие — полезный инструмент для поиска непроверенных частей кодовой базы. Тестовый охват мало полезен в качестве числового заявления о том, насколько хороши ваши тесты. “Нормальным” считается покрытие в пределах 80%.
Наиболее популярные инструменты для измерения покрытия кода, написанного на JavaScript istanbul.js blanket.js JSCover
Выбор фреймворка для создания тестов
На сегодняшний день доступна целая масса фреймворков для тестирования JavaScript-кода (overview).
Учитывая подобное разнообразие, выбор того или иного фреймворка, как правило, напрямую зависит от непосредственной задачи, которую мы ставим перед собой в процессе написания тестов. Идеально, когда функционал фреймворка покрывает несколько или все поставленные задачи (единая среда).
Например, поддерживаемая структура тестов. Если мы говорим о поддержке BDD, то следует выбирать среди Mocha, Jasmine, Jest, Сucumber.
Также доступны на выбор несколько assertion библиотек: Chai, Jasmine, Jest, Unexpected.
Если для вас важную роль играет представление и отображение результатов ваших проверок, то наибольший функционал в данной области предоставляют Mocha, Jasmine, Jest, Karma.
Для использования snap-shots в вашем тестировании следует обратить внимание на Jest или Ava.
Для борьбы с зависимостями и использования mocks и stubs следует обратить внимание на специализированные фреймворки типа Sinon.js, Jasmine, enzyme, Jest, testdouble.js.
Как уже упоминалось выше для измерения охвата и покрытия кода тестами возможно использование Istanbul, Jest.
Для функциональных тестов, для создания пользовательских сценариев поведения, необходимо использование браузерной среды или браузерной среды с программируемым API, что доступно в рамках Protractor, Nightwatch, Phantom.js, Сasper.
Описание некоторых фреймворков
JSDOM является реализацией JavaScript-стандартов WHATWG DOM и HTML. Другими словами, JSDom имитирует среду браузера, не запуская ничего, кроме простого JS. В этой моделируемой среде браузера тесты могут выполняться очень быстро. Недостатком JSDom является то, что не все может быть смоделировано вне реального браузера (например, вы не можете сделать снимок экрана), поэтому его использование ограничивает доступность ваших тестов.
Istanbul — расскажет вам, сколько вашего кода покрывается модульными тестами. Он будет сообщать о показателях, линиях, функциях в процентах, чтобы вы лучше поняли, что осталось покрыть.
Phantom.js — реализует «headless» браузер Webkit, который находится между реальным браузером и JSDom в скорости и стабильности. Достаточно популярен.
Karma — позволяет запускать тесты в браузерах, включая настоящие браузеры, Phantom, JSdom и даже устаревшие браузеры. Karma размещает тестовый сервер со специальной веб-страницей для запуска тестов в среде страницы. Эта страница может быть запущена во многих браузерах. Это также означает, что тесты можно запускать удаленно с помощью таких служб, как BrowserStack.
Chai — самая популярная assertion библиотека.
Unexpected — это также assertion библиотека с немного отличающимся синтаксисом от Chai.
Sinon.js — это набор очень мощных тестовых шпионов, заглушек и макетов (mocks) для модульного тестирования.
testdouble.js — представляет собой новую библиотеку, похожую на Sinon, с несколькими отличиями в дизайне, философии и особенностях, которые могли бы пригодиться во многих случаях.
Jasmine — представляет собой платформу тестирования, обеспечивающую все, что вам требуется для ваших тестов: работающая среда, структура, отчетность, assertion и mocks инструменты.
Mocha — в настоящее время является наиболее часто используемой библиотекой. В отличие от Jasmine, она используется со сторонними библиотеками mocks и assertions (обычно Enzyme и Chai). Это означает, что Mocha немного сложнее настроить, но она более гибкая и открыта для расширений.
Jest — это платформа тестирования, рекомендованная Facebook. Он использует функционал Jasmine и добавляет функции поверх него, поэтому все упоминания о Jasmine относится и к нему.
Ava — минималистическая библиотека, которая имеет возможность запускать тесты параллельно.
Selenium — автоматизирует браузер для имитации поведения пользователя. Он не написан специально для тестов и может управлять браузером для многих целей, предоставляя сервер, который имитирует поведение пользователя в браузере с использованием API. Selenium можно контролировать разными способами и использовать различные языки программирования, а также с некоторыми инструментами даже без реального программирования.
Protractor — это библиотека, которая использует Selenium, но добавляет улучшенный синтаксис и специально встроенные хуки для Angular.
Nightwatch — имеет собственную реализацию selenium WebDriver. И обеспечивает собственную среду тестирования, тестовый сервер, assertion и другие инструменты.
Сasper — написан поверх Phantom и Slimer (так же, как Phantom, но в Gecko FireFox), чтобы при помощи специальных утилиты более просто создавать Phantom и Slimer скрипты. Каспер предоставляет нам более быстрый, но менее стабильный способ запуска функциональных тестов в браузерах с интерфейсом UI.
Сucumber — еще один замечательный фреймворк для функционального тестирования. Включает в себя весь основной функционал, ранее перечисленный в похожих фреймворках.
Литература:
Книги
Статьи
Когда использовать mocks в юнит-тестировании
Использование моков в модульном тестировании является спорной темой. Автор оригинала заметил, что на протяжении всей своей карьеры в программировании он сначала перешел от моков почти над каждой зависимостью к политике «без моков», а затем к «только моки для внешних зависимостей».
Ни одна из этих практик не является достаточно хорошей. В этой статье Владимир Хориков покажет, какие зависимости следует мокать, а какие использовать как есть в тестах.
Что такое mock (мок, от англ. «пародия», «имитация»)?
Прежде чем перейти к теме того, когда использовать моки, давайте обсудим, что такое мок. Люди часто используют термины тестовый двойник (test double) и мок (mock) как синонимы, но технически это не так:
- Тестовый двойник — это всеобъемлющий термин, который описывает все виды фальшивых (fake) зависимостей, непригодных к использованию в конечном продукте (non-production-ready), в тестах. Такая зависимость выглядит и ведет себя как ее аналог, предназначенный для production, но на самом деле является упрощенной версией, которая снижает сложность и облегчает тестирование. Этот термин был введен Джерардом Месаросом в его книге «xUnit Test Patterns: Refactoring Test Code». Само название происходит от понятия дублера в кино.
- Мок – это лишь один из видов таких зависимостей.
Согласно Жерару Месарошу, существует 5 видов тестовых двойников:
- Пустышка (dummy)
- Стаб (stub)
- Шпион (spy)
- Мок (mock)
- Фейк (fake)
Такое разнообразие может показаться пугающим, но на самом деле все они могут быть сгруппированы всего в два типа: моки и стабы.
Разница между этими двумя типами сводится к следующему:
- Моки помогают имитировать и изучать исходящие (outcoming) взаимодействия. То есть вызовы, совершаемые тестируемой системой (SUT) к ее зависимостям для изменения их состояния.
- Стабы помогают имитировать входящие (incoming) взаимодействия. То есть вызовы, совершаемые SUT к ее зависимостям для получения входных данных.
Например, отправка электронной почты является исходящим (outcoming) взаимодействием: это взаимодействие приводит к побочному эффекту на SMTP-сервере. Тестовый двойник, имитирующий такое взаимодействие, — это мок.
Извлечение данных из БД является входящим (incoming) взаимодействием — оно не приводит к побочному эффекту. Соответствующий тестовый двойник является стабом.
Все остальные различия между пятью типами тестовых двойников являются незначительными деталями реализации:
- Spies (шпионы) выполняют ту же роль, что и моки. Отличие в том, что spies пишутся вручную, а моки создаются с помощью готовых инструментов. Иногда spies называют рукописными моками.
С другой стороны, разница между стабами, dummy (пустышками) и фейками заключается в том, насколько они умны:
- Dummy — это простое, жестко закодированное значение, такое как null значение или выдуманная строка. Он используется для удовлетворения сигнатуры метода SUT и не участвует в получении конечного результата.
- Стаб посложнее. Это полноценная зависимость, которую вы настраиваете для возврата разных значений для разных сценариев.
- Фейк — это то же самое, что и стаб для большинства целей. Разница заключается в причинах его создания: фейк обычно используется для замены еще не существующей зависимости.
Обратите внимание на разницу между моками и стабами (помимо исходящих и входящих взаимодействий). Моки помогают эмулировать и изучать взаимодействия между SUT и его зависимостями, в то время как стабы помогают только эмулировать эти взаимодействия. Это важное различие.
Мок-как-инструмент vs. мок-как-тестовый-двойник
Термин мок перегружен и может означать разные вещи в разных обстоятельствах. В статье уже упоминалось, что люди часто используют этот термин для обозначения любого тестового двойника, тогда как моки — это всего лишь подмножество тестовых двойников.
Но у термина мок есть и другое значение. Вы также можете ссылаться на классы из библиотек (для создания моков) как на моки. Эти классы помогают вам создавать настоящие моки, но сами по себе они не являются моками как таковыми:
[Fact] public void Sending_a_greetings_email() < // Using a mock-the-tool to create a mock-the-test-double var mock = new Mock(); var sut = new Controller(mock.Object); sut.GreetUser("user@email.com"); // Examining the call from the SUT to the test double mock.Verify( x => x.SendGreetingsEmail("user@email.com"), Times.Once); >
В этом тесте используется класс Mock из библиотеки Moq. Этот класс представляет собой инструмент, который позволяет вам создавать тестовый двойник — мок. Другими словами, класс Mock (или Mock ) является мок-как-инструмент, в то время как экземпляр этого класса является мок-как-тестовый-двойник.
Важно не смешивать мок-как-инструмент с мок-как-тестовый-двойник, потому что вы можете использовать мок-как-инструмент для создания обоих типов тестовых двойников: моков и стабов.
Вот еще один пример теста, в котором используется класс Mock. Экземпляр этого класса — стаб, а не мок:
[Fact] public void Creating_a_report() < // Using a mock-the-tool to create a stub var stub = new Mock(); // Setting up a canned answer stub.Setup(x => x.GetNumberOfUsers()).Returns(10); var sut = new Controller(stub.Object); Report report = sut.CreateReport(); Assert.Equal(10, report.NumberOfUsers); >
Этот тестовый двойник имитирует входящее взаимодействие – вызов, который предоставляет SUT входные данные. С другой стороны, в предыдущем примере вызов SendGreetingsEmail() является исходящим взаимодействием. Его единственная цель — вызвать побочный эффект — отправить электронное письмо.
Не проверяйте взаимодействия со стабами
Как уже упоминал выше, моки помогают эмулировать и изучать исходящие взаимодействия между SUT и его зависимостями, в то время как стабы помогают только эмулировать входящие взаимодействия, а не изучать их.
Из этого следует, что вы никогда не должны проверять взаимодействие со стабами. Вызов от SUT к стабу не является частью конечного результата, который выдает SUT. Такой вызов — это всего лишь средство для получения конечного результата; это деталь реализации. Проверка взаимодействий со стабами является распространенным анти-паттерном, который приводит к хрупким тестам.
Единственный способ избежать хрупкости тестов — это заставить эти тесты проверять конечный результат (который в идеале должен иметь значение для непрограммиста), а не детали реализации.
В приведенных выше примерах проверка
mock.Verify(x => x.SendGreetingsEmail("user@email.com"))
соответствует фактическому результату, и этот результат имеет значение для специалиста в предметной области: отправка приветственного электронного письма — это то, что такой специалист хотели бы, чтобы система делала.
В то же время вызов GetNumberOfUsers() вообще не является результатом. Это внутренняя деталь реализации, касающаяся того, как SUT собирает данные, необходимые для создания отчета. Следовательно, проверка этого вызова приведет к уязвимости теста. Неважно, как SUT генерирует конечный результат, если этот результат правильный.
Вот пример такого хрупкого теста:
[Fact] public void Creating_a_report() < var stub = new Mock(); stub.Setup(x => x.GetNumberOfUsers()).Returns(10); var sut = new Controller(stub.Object); Report report = sut.CreateReport(); Assert.Equal(10, report.NumberOfUsers); // Asserting an interaction with a stub stub.Verify( x => x.GetNumberOfUsers(), Times.Once);
Такая практика проверки того, что не является частью конечного результата, также называется чрезмерной спецификацией.
Моки — это более сложная тема: не все их использования приводят к уязвимости теста, но многие из них делают это. Вы скоро поймете почему.
Совместное использование моков и стабов
Иногда нужно создать тестовый двойник, который проявляет свойства как мока, так и стаба:
[Fact] public void Purchase_fails_when_not_enough_inventory() < var storeMock = new Mock(); // Setting up a canned answer storeMock .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5)) .Returns(false); var sut = new Customer(); bool success = sut.Purchase(storeMock.Object, Product.Shampoo, 5); Assert.False(success); // Examining a call from the SUT to the mock storeMock.Verify( x => x.RemoveInventory(Product.Shampoo, 5), Times.Never); >
Этот тест использует storeMock для двух целей: он возвращает шаблонный ответ и проверяет вызов метода, сделанный SUT.
Однако обратите внимание, что это два разных метода: тест устанавливает ответ от HasEnoughInventory(), но затем проверяет вызов RemoveInventory(). Таким образом, здесь не нарушается правило не проверять взаимодействия со стабами.
Когда тестовый двойник является одновременно и моком, и стабом, он все равно называется моком. Это в основном потому, что нужно выбрать одно имя, но также и потому, что являться моком — более важный факт.
Mocks vs. stubs и commands vs. queries
Понятие моков и стабов связано с принципом command-query separation (CQS). Принцип CQS гласит, что каждый метод должен быть либо командой, либо запросом, но не обоими:
- Команды — это методы, которые вызывают побочные эффекты и не возвращают никакого значения. Примеры побочных эффектов включают изменение состояния объекта, изменение файла в файловой системе и т. д.
- Запросы противоположны этому — они не имеют побочных эффектов и возвращают значение.
Другими словами, задавая вопрос, вы не должны менять ответ. Код, который поддерживает такое четкое разделение, становится легче для чтения. Тестовые двойники, заменяющие команды, становятся моками. Аналогично, тестовые двойники, заменяющие запросы, являются стабами:
Посмотрите еще раз на два теста из предыдущих примеров:
var mock = new Mock(); mock.Verify(x => x.SendGreetingsEmail("user@email.com")); var stub = new Mock(); stub.Setup(x => x.GetNumberOfUsers()).Returns(10);
SendGreetingsEmail() — это команда, побочным эффектом которой является отправка электронного письма. Тестовый двойник, который заменяет эту команду, является моком.
С другой стороны, GetNumberOfUsers() — это запрос, который возвращает значение и не изменяет состояние базы данных. Соответствующий тестовый двойник — стаб.
Когда мокать?
Разобравшись со всеми этими определениями, давайте поговорим о том, когда вам следует использовать моки.
Очевидно, что вы не хотите мокать саму тестируемую систему (SUT), поэтому вопрос «Когда мокать?» сводится к следующему: «Какие типы зависимостей вы должны заменять на моки, а какие использовать в тестах?»
Вот все типы зависимостей модульного тестирования, которые автор оригинала перечислил в своей предыдущей статье:
- Совместная зависимость (shared dependency) — это зависимость, которая является общей между тестами и с помощью нее тесты могут влиять на результаты друг друга.
- Приватная зависимость (private dependency) — это любая зависимость, которая не является совместной.
Совместная зависимость соответствует изменяемой внепроцессорной зависимости (mutable out-of-process dependency) в подавляющем большинстве случаев, поэтому автор оригинала использует здесь эти два понятия как синонимы. (Ознакомьтесь с предыдущим постом Владимира Хорикова, чтобы узнать больше: Unit Testing Dependencies: The Complete Guide.)
Существуют две школы модульного тестирования с собственными взглядами на то, какие типы зависимостей следует заменять на моки:
- Лондонская школа (также известная как школа mockist) выступает за замену всех изменяемых зависимостей на моки.
- Классическая школа (также известная как школа Детройта) выступает за замену только общих (изменяемых внепроцессорных) зависимостей.
Обе школы ошибаются в своем отношении к мокам, хотя классическая школа меньше, чем лондонская.
Моки и неизменяемые внепроцессорные зависимости.
А как насчет иммутабельных внепроцессорных (immutable out-of-process) зависимостей? Разве их не стоит мокать, по крайней мере, по мнению одной из школ? Неизменяемые внепроцессорные зависимости (например, служба API только для чтения) следует заменить тестовым двойником, но этот тестовый двойник будет стабом, а не моком.
Это опять же из-за различий между моками и стабами:
· Моки предназначены для исходящих взаимодействий (команд) — взаимодействий, которые оставляют побочный эффект в зависимости.
· Стабы предназначены для входящих взаимодействий (запросов) — взаимодействий, которые не оставляют побочных эффектов в зависимости.
Взаимодействия с иммутабельными внепроцессорными зависимостями по определению являются входящими и, следовательно, не должны проверяться в тестах, а только заменяться шаблонными ответами (обе школы с этим согласны).
Сначала Владимир Хориков опишет, почему лондонская школа ошибочна, а затем — почему классический подход тоже неверен.
Не мокайте все изменяемые зависимости
Не стоит мокать все изменяемые зависимости. Чтобы понять, почему, нам нужно рассмотреть два типа коммуникаций в типичном приложении: внутрисистемный и межсистемный.
- Внутрисистемные (Intra-system) коммуникации — это коммуникации между классами внутри вашего приложения.
- Межсистемные (Inter-system) коммуникации — это когда ваше приложение взаимодействует с другими приложениями.
Между ними существует огромная разница: внутрисистемные коммуникации являются деталями реализации; межсистемные коммуникации — нет.
Внутрисистемные коммуникации являются деталями реализации, поскольку взаимодействие, через которое проходят ваши доменные классы для выполнения операции, не является частью их наблюдаемого поведения. Это взаимодействие не имеет непосредственной связи с целью клиента. Таким образом, связь с таким взаимодействием приводит к хрупким тестам.
Межсистемные коммуникации — это совсем другое дело. В отличие от взаимодействия между классами внутри вашего приложения, то, как ваша система взаимодействует с внешним миром, формирует наблюдаемое поведение этой системы в целом. Это часть контракта, который ваше приложение должно соблюдать в любое время.
Это свойство межсистемных коммуникаций проистекает из того, как отдельные приложения развиваются вместе. Один из основных принципов такой эволюции – обеспечение обратной совместимости. Независимо от рефакторинга, который вы выполняете внутри своего приложения, паттерн взаимодействия, который оно использует для взаимодействия с внешними приложениями, должен всегда оставаться на месте, чтобы внешние приложения могли его понять. Например, сообщения, отправляемые вашим приложением по шине, должны сохранять свою структуру и так далее.
Использование моков закрепляет контракт взаимодействия между тестируемой системой и зависимостью. Это именно то, что вам нужно при проверке связи между вашей системой и внешними приложениями. И наоборот, использование моков для проверки связи между классами внутри вашей системы связывает ваши тесты с деталями реализации, делая их хрупкими. Внутрисистемные связи соответствуют изменяемым внутрипроцессорным зависимостям:
Итак, лондонская школа ошибается, потому что она поощряет использование моков для всех изменяемых зависимостей и не проводит различия между внутрисистемными (внутрипроцессорными) и межсистемными (внепроцессорными) коммуникациями.
В результате тесты проверяют связь между классами точно так же, как они проверяют связь между вашим приложением и внешними системами. Это неизбирательное использование моков является причиной того, что следование лондонской школе часто приводит к хрупким тестам — тестам, которые связаны с деталями реализации.
Не мокайте все внепроцессорные зависимости
Классическая школа лучше справляется с этим вопросом, потому что она выступает за замену только внепроцессорных зависимостей, таких как служба SMTP, шина сообщений и т. д. Но классическая школа также не идеальна по отношению к межсистемным коммуникациям. Эта школа также поощряет чрезмерное использование моков, хотя и не так сильно, как лондонская школа.
Не все внепроцессорные зависимости следует мокать. Если внепроцессорная зависимость доступна только через ваше приложение, то связь с такой зависимостью не является частью наблюдаемого поведения вашей системы. Внепроцессорная зависимость, которая не может наблюдаться извне, по сути, действует как часть вашего приложения. Связи с такой зависимостью становятся деталями реализации: они не должны оставаться неизменными после рефакторинга и, следовательно, не должны проверяться с помощью моков.
Помните, что требование всегда сохранять схему взаимодействия между вашим приложением и внешними системами проистекает из необходимости поддерживать обратную совместимость. Вы должны поддерживать способ взаимодействия вашего приложения с внешними системами. Это потому, что вы не можете изменять эти внешние системы одновременно с вашим приложением; они могут следовать другому циклу развертывания, или вы можете просто не контролировать их.
Но когда ваше приложение действует как прокси-сервер для внешней системы, и ни один клиент не может получить к ней прямой доступ, требование обратной совместимости исчезает. Теперь вы можете развернуть свое приложение вместе с этой внешней системой, и это не повлияет на клиентов. Схема взаимодействия с такой системой становится деталью реализации.
Хорошим примером здесь является БД приложения: БД, которая используется только вашим приложением. Ни одна внешняя система не имеет доступа к этой БД. Таким образом, вы можете изменить схему взаимодействия между вашей системой и БД приложения любым удобным вам способом, если это не ломает существующую функциональность. Поскольку эта БД полностью скрыта от глаз клиентов, вы даже можете заменить ее совершенно другим механизмом хранения, и никто этого не заметит.
Использование моков для внепроцессорных зависимостей, которые вы полностью контролируете, также приводит к хрупким тестам. Вы не хотите, чтобы ваши тесты становились красными каждый раз, когда вы разбиваете таблицу в БД или изменяете тип одного из параметров в хранимой процедуре. БД и ваше приложение должны рассматриваться как одна система.
Это различие разделяет внепроцессорные зависимости на две подкатегории:
- Управляемые (managed) зависимости — внепроцессорные зависимости, над которыми вы имеете полный контроль. Эти зависимости доступны только через ваше приложение; взаимодействия с ними не видны внешнему миру. Типичным примером является БД приложения. Внешние системы не имеют прямого доступа к вашей БД; они делают это через API, предоставляемый вашим приложением.
- Неуправляемые (unmanaged) зависимости — внепроцессорные зависимости, над которыми у вас нет полного контроля. Взаимодействия с такими зависимостями наблюдаются извне. Примеры — SMTP-сервер и шина сообщений: оба они создают побочные эффекты, видимые для других приложений.
Только неуправляемые зависимости должны быть заменены моками. Используйте реальные экземпляры управляемых зависимостей в тестах.
Резюме
Тестовый двойник — это всеобъемлющий термин, который описывает все виды непригодных к использованию в конечном продукте (non-production-ready), фейковых зависимостей в тестах.
- Существует пять вариантов тестовых двойников — dummy (манекен), стаб, spy (шпион), мок и фейк, которые можно сгруппировать всего в два типа: моки и стабы.
- Spies функционально такие же, как и моки; dummy и фейки выполняют ту же роль, что и стабы.
Различия между моками и стабами:
- Моки помогают имитировать и изучать исходящие взаимодействия: вызовы от SUT к его зависимостям, которые изменяют состояние этих зависимостей.
- Стабы помогают имитировать входящие взаимодействия: для получения входных данных SUT обращается к своим зависимостям.
- Проверка взаимодействия со стабами всегда приводит к хрупким тестам.
- Тестовые двойники, заменяющие команды CQS, являются моками. Тестовые двойники, заменяющие запросы CQS, являются стабами.
Мок-как-инструмент — это класс из библиотеки моков, который вы можете использовать для создания мока-как-тестовый-двойник или стаба.
Внепроцессорные зависимости можно разделить на 2 подкатегории: управляемые и неуправляемые зависимости.
- Управляемые зависимости — это внепроцессорные зависимости, доступные только через ваше приложение. Взаимодействия с управляемыми зависимостями не наблюдаются извне. Типичным примером является БД приложения.
- Неуправляемые зависимости — это внепроцессорные зависимости, к которым имеют доступ другие приложения. Взаимодействия с неуправляемыми зависимостями наблюдаются извне. Типичные примеры — SMTP-сервер и шина сообщений.
- Связь с управляемыми зависимостями — это детали реализации; связь с неуправляемыми зависимостями является частью наблюдаемого поведения вашей системы.
- Используйте реальные экземпляры управляемых зависимостей в интеграционных тестах; замените неуправляемые зависимости моками.
- и
- автоматическое тестирование
- юнит-тесты
- юнит-тестирование
- моки
- стабы
- Тестирование IT-систем
- Программирование
- Анализ и проектирование систем
- Проектирование и рефакторинг
- Тестирование веб-сервисов
Mockito
Mockito — фреймворк для тестирования приложений, который позволяет легко и быстро подменять реальные объекты программы «пустышками». Такие фиктивные объекты часто называют «моками» (Mock — подражать).
Зачем используется Mockito
Инструмент упрощает разработку юнит-тестов для классов с внешними зависимостями. Фиктивная реализация интерфейса, статического метода или класса, которую производит Mockito, позволяет определить вывод конкретных вызовов методов: фиктивные классы записывают взаимодействие с системой, а тесты могут его проверить и подтвердить.
Профессия / 16 месяцев
Тестировщик-автоматизатор
Лучший выбор для быстрого старта в IT
Подключение Mockito
Обычно библиотека подключается к системе сборки приложений, которой пользуется разработчик, — Maven или Gradle. В случае с Gradle установка Mockito выглядит так:
При использовании Maven библиотека добавляется в зависимости таким образом:
В случае совместного использования JUnit 5 и Mockito в Maven также добавляется следующая зависимость:
Кроме систем сборки, библиотеку можно подключить к любой интегрированной среде разработки. Все популярные IDE — Eclipse, Android Studio, Visual Studio Code, IntelliJ IDEA — поддерживают как Mockito, так и Maven, JUnit и Gradle.
Станьте тестировщиком – это лучший выбор для быстрого старта в IT
Базовые понятия тестирования
Юнит-тесты позволяют проверять поведение определенных классов или методов в отрыве от их зависимостей. Так как тестируют самые маленькие «элементы» кода, не нужно использовать настоящие реализации зависимостей. Более того, чтобы протестировать различные модели поведения, часто требуется применять немного разные реализации этих зависимостей. Традиционный подход к тестированию — создание «заглушек», конкретных реализаций интерфейса, подходящих для данного сценария. Такие реализации имеют жестко закодированную логику. Заглушка — разновидность тестового двойника (как и фиктивные объекты, макеты, шпионы и так далее). В Mockito чаще всего используются два типа тестовых двойников — макеты (mocks) и шпионы (spies).
Макеты (моки)
Такое тестирование называют мокингом. Создаются объекты-имитаторы, которые реализуют поведение реальной подсистемы. Моки используются как замена зависимостей.
С помощью Mockito разработчик создает имитатор — мок, указывает библиотеке, что делать при вызове определенных методов, а затем использует экземпляр имитатора в своем тесте вместо реального объекта. По умолчанию Mockito предоставляет реализацию для каждого метода mock. После тестирования можно запросить mock, чтобы узнать, какие конкретные методы были вызваны, или проверить побочные эффекты в виде изменения состояния.
Шпионы
Шпион — второй тестовый двойник, который создает Mockito. Для этого требуется экземпляр объекта, за которым можно наблюдать — шпионить. По умолчанию шпион делегирует все вызовы методов реальному объекту и записывает, какой метод был вызван и какие имел параметры.
Шпионы полезны для тестирования устаревшего кода. Но если приходится использовать шпион для частичного моделирования класса, значит, класс выполняет слишком много действий. Это идет вразрез с принципом единой ответственности.
Создание моков с помощью Mockito API
Библиотека Mockito позволяет создавать mock-объекты разными методами:
- с применением расширения @ExtendWith(MockitoExtension.class) для JUnit 5 в сочетании с аннотацией @Mock;
- помощью статического метода mock();
- использованием аннотации @Mock.
При использовании аннотации @Mock нужно подготовить к работе аннотированные поля. Расширение MockitoExtension делает это, вызывая статический метод MockitoAnnotations.initMocks().
Разберем использование методов на модели данных:
package com.example.junit5;
public class Database public boolean isAvailable() return false;
>
public int getUniqueId() return 45;
>
>
package com.example.junit5;
public class Service private Database database;
public Service(Database database) this.database = database;
>
public boolean query(String query) return database.isAvailable();
>
@Override
public String toString() return «Используется база данных с ID: » + String.valueOf(database.getUniqueId());
>
>
Модульный тест в Mockito для объекта Database может выглядеть так:
package com.example.junit5;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ServiceTest @Mock
Database databaseMock;
@Test
public void testQuery() assertNotNull(databaseMock);
when(databaseMock.isAvailable()).thenReturn(true);
Service t = new Service(databaseMock);
boolean check = t.query(«* from t»);
assertTrue(check);
>
>
Код теста выполняет действия:
- дает Mockito указание создать макеты на основе аннотации @Mock — для этого требуется JUnit 5. Если используется другая версия JUnit, нужно вызвать Mock.init() в методе setup;
- сообщает Mockito, что нужно создать фиктивный экземпляр базы данных — databaseMock;
- настраивает Mock на возврат true при вызове его метода isAvailable;
- выполняет код тестируемого класса;
- утверждает, что вызов метода вернул true.
Настройка возвращаемых значений методов
Mockito API позволяет настраивать возвращаемые значения методов, которые вызываются для фиктивных объектов. Неопределенные вызовы методов возвращают пустые значения:
- null для объектов;
- 0 для чисел;
- false для логических значений;
- пустые коллекции для коллекций.
Использование when().thenReturn() и when().thenThrow()
Моки могут возвращать различные значения в зависимости от аргументов, переданных в метод. Цепочка методов when(…).thenReturn(…) используется для указания возвращаемого значения для вызова метода с заранее заданными параметрами. Для возврата значений также можно использовать такие методы, как anyString или anyInt.
Создание фиктивных финальных классов и статических методов
После того как библиотека mockito-inline пришла на смену mockito-core, у пользователей появилась возможность создавать моки финальных классов и статических методов. Предположим, в приложении есть такой финальный класс:
final class FinalClass public final String finalMethod() < return «строка текста»; >
>
С помощью приведенного ниже кода можно создать мок этого класса:
package com.example.junit5;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class MockitoMockFinal @Test
public void testMockFinal(@Mock FinalClass finalMocked) assertNotNull(finalMocked);
>
@Test
public void testMockFinalViaMockStatic() MockedStatic mockStatic = Mockito.mockStatic(FinalClass.class);
assertNotNull(mockStatic);
>
>
Mockito делает код тестов проще и понятнее благодаря использованию фиктивных интерфейсов, прослушивающих вызовов, сопоставителей и захватчиков аргументов. Но, как и любой другой мощный инструмент, он должен использоваться правильно, чтобы быть максимально полезным.
Тестировщик-автоматизатор
Как ворваться в IT, даже если вы не умеете программировать? Стать тестировщиком. Для старта достаточно базовых знаний ПК. А начать работать можно уже через 4 месяца обучения.
Статьи по теме: