Unit-тестирование. Примеры и лучшие практики

Что такое модульное тестирование?

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

«Модулями» в модульном тестировании могут быть функции, процедуры, методы, объекты или другие сущности в исходном коде приложения. Каждая команда разработчиков решает, какие модули наиболее подходят для понимания и тестирования ее системы. Например, в объектно-ориентированном программировании, в качестве модуля обычно рассматривается класс. Чтобы тесты оставались простыми и предсказуемыми, при проведении модульного тестирования используются объекты-макеты, имитирующие другие части кода или модули.

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

Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.

Основными этапами выполнения unit-тестов являются:

  1. Планирование и настройка окружения
  2. Написание тест-кейсов и скриптов
  3. Выполнение тест-кейсов с помощью фреймворка тестирования
  4. Анализ результатов

Некоторые преимущества модульного тестирования:

  • Раннее обнаружение проблем в цикле разработки
  • Снижение стоимости
  • Разработка на основе тестирования
  • Более частые релизы
  • Более легкий рефакторинг кода
  • Обнаружение изменений, которые могут нарушить проектирование по контракту
  • Снижение неопределенности
  • Документирование поведения системы

Это часть обширной серии руководств о CI/CD.

Содержание:

Как работают unit-тесты

Модульное тестирование  обычно состоит из четырех этапов:

  1. Планирование и настройка среды  –  разработчики обдумывают какие блоки кода им нужно протестировать и как покрыть всю необходимую функциональность каждого модуля, чтобы протестировать его эффективно.
  2. Написание тест-кейсов и скриптов   разработчики пишут код unit-тестов и готовят скрипты для отладки кода.
  3. Выполнение модульного тестирования – unit-тест запускается и показывает работу кода в каждом тест-кейсе.
  4. Анализ результатов – разработчики могут выявить ошибки или проблемы в коде и исправить их.

Разработка через тестирование (Test-driven development, TDD) – это распространенный подход к модульному тестированию. Он требует, чтобы разработчик создал unit-тест еще до того, как будет написан код приложения. Естественно, этот первоначальный тест будет провален. Затем разработчик добавляет соответствующую функциональность в приложение до тех пор, пока тесты не пройдут. TDD обычно приводит к созданию высококачественной и последовательной кодовой базы.

Критерии эффективности модульного тестирования:

  • Запуск каждого тест-кейса изолированно, с “заглушками” или “моками” для имитации внешних зависимостей. Это гарантирует, что unit-тесты  учтут  функциональность только текущего тестируемого модуля.
  • Тестирование не каждой строчки кода,  а фокусирование на значимых фичах тестируемого модуля. В целом, unit-тестирование должно быть сосредоточено на коде, который влияет на поведение всего программного продукта.
  • Проверка каждого тест-кейса с помощью определенных критериев в коде, известных как “утверждения”. Тестовый фреймворк  использует их для запуска теста и сообщения о неудачных тестах.
  • Запуск тест-кейсов часто и на ранних этапах жизненного цикла разработки.

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

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

К преимуществам модульного тестирования относятся:

  • Обнаружение проблем на ранних этапах цикла разработки – модульное тестирование помогает выявить дефекты и ошибки на ранних этапах цикла разработки программного обеспечения. Раннее обнаружение ошибок очень важно, поскольку позволяет устранить дефекты до того, как они перерастут в более сложные ошибки на более поздних этапах разработки.
  • Снижение затрат – благодаря раннему выявлению дефектов модульное тестирование позволяет значительно снизить стоимость их исправления. Как правило, исправление дефектов на более поздних этапах разработки или после развертывания программного обеспечения обходится дороже.
  • Поощрение разработки через тестирование – unit-тестирование является основным компонентом разработки через тестирование (TDD), когда тесты пишутся до написания реального кода. Такой подход гарантирует, что кодовая база спроектирована так, чтобы пройти тесты. Это, в свою очередь, приводит к созданию более структурированного, надежного и легкого в сопровождении кода.
  • Возможность более частых релизов – благодаря полному набору unit-тестов разработчики могут вносить изменения в код с большей уверенностью. Это снижает риски, связанные с выпуском новых версий, что позволяет чаще обновлять и улучшать программное обеспечение.
  • Возможность рефакторинга кода – unit-тесты обеспечивают безопасность, которая позволяет разработчикам проводить рефакторинг кода уверенно. Знание того, что изменения можно быстро протестировать, чтобы убедиться, что они не сломают существующую функциональность, стимулирует улучшать и оптимизировать код, не опасаясь появления ошибок.
  • Обнаружение изменений, нарушающих проектирование по контракту – unit-тесты могут помочь в выявлении изменений в коде, которые могут нарушить задуманный дизайн или проект системы. Это гарантирует, что отдельные компоненты программного обеспечения работают так, как ожидалось, и гармонично сочетаются друг с другом.
  • Уменьшение неопределенности – благодаря надежному процессу модульного тестирования разработчики получают уверенность в качестве и работоспособности своего кода. Это уменьшает неопределенность, особенно при внесении изменений или добавлении новых функций.

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

Чем модульное тестирование отличается от других видов тестирования?

Unit-тестирование vs. Интеграционное тестирование

Интеграционное тестирование включает в себя тестирование программных модулей и взаимодействие между ними. При этом тестируются группы логически связанных модулей.

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

Основное различие между unit-тестами и интеграционными тестами заключается в том, что и как они проверяют:

  • Unit-тесты тестируют отдельный фрагмент кода, а интеграционные тесты тестируют модули кода, чтобы понять, как они работают по отдельности и взаимодействуют друг с другом.
  • Unit-тесты  выполняются быстро и легко, потому что они “имитируют” внешние зависимости. Интеграционные тесты более сложны и требуют больше ресурсов для выполнения, потому что они должны учитывать как внутренние, так и внешние (“реальные”) зависимости.

Unit-тестирование vs. Функциональное тестирование

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

Разработчики программного обеспечения используют функциональное тестирование как способ обеспечения качества (QA). Как правило, если система проходит функциональное тестирование, она считается готовой к выпуску. Функциональное тестирование важно тем, что оно старается в точности повторять реальный пользовательский опыт, а значит проверяет, соответствует ли приложение требованиям заказчика.

Разницу между unit-тестированием и функциональным тестированием можно сформулировать следующим образом:

  • Unit-тесты предназначены для тестирования отдельных частей кода в изоляции. Их легко и быстро создавать, они помогают находить и исправлять дефекты на ранних этапах разработки. Обычно они запускаются вместе с каждой сборкой программного обеспечения. Однако, они не заменяют функциональное тестирование, поскольку не проверяют приложение от начала до конца.
  • Функциональное тестирование направлено на проверку функциональности всего приложения. Его создание занимает много времени и требует значительных вычислительных ресурсов для выполнения, но оно очень полезно для тестирования всего приложения. Функциональное тестирование является неотъемлемой частью набора автотестов, но обычно используется на более поздних этапах жизненного цикла разработки и выполняется реже, чем unit-тесты.

Unit-тестирование vs. Регрессионное тестирование

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

Unit-тесты используются разработчиками для проверки функциональности различных компонентов в коде. Это гарантирует, что все переменные, функции и объекты работают так, как ожидается.

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

Можно ли использовать модульное тестирование для обеспечения безопасности?

Unit-тесты принято создавать в процессе разработки. Однако, эти тесты обычно проверяют только функциональность, а не другие аспекты кода, такие как безопасность. Многие организации используют подход “сдвига влево”, при котором важные аспекты программного проекта должны быть протестированы как можно раньше в жизненном цикле разработки программного обеспечения, когда их легко исправить.

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

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

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

Техники модульного тестирования

Структурное модульное тестирование

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

Основные методы структурного тестирования:

  • Тестирование операторов, ветвей и путей – каждый оператор, ветвь или путь в программе тестируются  по крайней мере один раз. Тестирование операторов  – является наиболее детализированным вариантом.
  • Условное тестирование позволяет разработчику выбирать путь, выполняемый тестом, путем выполнения кода на основе сравнения значений.
  • Тестирование выражений – проверка приложения на различные значения регулярного выражения.

Функциональное unit-тестирование

Функциональное unit-тестирование – это метод тестирования “черного ящика” для проверки функциональности компонента приложения. 

Основные техники функционального тестирования:

  • Тестирование области ввода – проверяет размер и тип входных данных и сравнивает их с классами эквивалентности.
  • Анализ граничных значений — проверка правильности реакции программного обеспечения на входные данные, выходящие за пределы граничных значений.
  • Проверка синтаксиса – тесты, которые проверяют, правильно ли программа интерпретирует синтаксис входных данных.
  • Эквивалентное разбиение – техника тестирования программного обеспечения, при которой входные данные делятся на различные группы, для каждой из которых выполняются тест-кейсы.

Техники тестирования, основанные на ошибках

Unit–тесты, основанные на ошибках,  лучше всего создавать разработчикам, которые изначально проектировали код.

К таким техникам относятся:

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

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

Разные системы поддерживают разные типы unit-тестов. 

Unit-тестирование Android приложений

Разработчики могут запускать unit-тесты на устройствах Android или других компьютерах. Существует два основных типа unit-тестов для Android.

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

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

Локальные unit-тесты  выполняются на сервере или компьютере разработчика. Обычно это небольшие, быстрые тесты на стороне хоста, которые изолируют тестируемый объект от других частей приложения. Большие локальные модульные тесты включают в себя запуск симулятора Android (например, Robolectric) локально на машине. 

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

// If the Start button is clicked
onView(withText("Start"))
    .perform(click())

// Then display the Hello message
onView(withText("Hello"))
    .check(matches(isDisplayed()))

Следующий фрагмент демонстрирует часть локального теста на стороне хоста для ViewModel:

// If given a ViewModel1 instance
val viewModel = ViewMode1(ExampleDataRepository)

// After loading data
viewModel.loadData()

// Expose data
assertTrue(viewModel.data != null)

Unit-тестирование Angular приложений

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

Пакет тестирования в Angular предлагает две утилиты: TestBed и async. TestBed – это основная утилита пакета Angular.

Контейнер “describe” включает в себя несколько блоков, таких как it, xit и beforeEach. Блок “beforeEach” запускается первым, а остальные блоки могут выполняться независимо. Первый блок из файла app.component.spec.ts – это beforeEach (внутри контейнера describe) и должен выполняться перед другими блоками.

Angular объявляет декларацию модуля приложения из файла app.module.ts в блоке beforeEach. Компонент приложения, смоделированный/объявленный в beforeEach, является самым важным компонентом тестового окружения

Элемент fixture.debugElement.componentInstance создаст экземпляр класса AppComponent. Тестировщик может использовать toBeTruthy, чтобы проверить, действительно ли система создает экземпляр класса:

beforeEach(async(() => {
   TestBed.configureTestingModule({
      declarations: [
         AppComponent
      ],
   }).compileComponents();
}));

После объявления целевого компонента в блоке beforeEach тестировщик, используя блок it, может проверить, создала ли система этот компонент.

Элемент fixture.debugElement.componentInstance создаст экземпляр класса AppComponent. Тестировщик может использовать toBeTruthy, чтобы проверить, действительно ли система создает экземпляр класса:

it('creates the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
}));

Следующий блок показывает доступ к свойствам компонента приложения. По умолчанию система добавляет только свойство title. Тестировщик может легко проверить соответствие заголовка в созданном компоненте: 

it(`title should be 'angular-unit-test'`, async(() => {
     const fixture = TestBed.createComponent(AppComponent);
     const app = fixture.debugElement.componentInstance;
     expect(app.title).toEqual('angular-unit-test');
}));

Четвертый блок демонстрирует поведение теста в браузере. Создав компонент detectChanges, система вызывает его экземпляр для имитации выполнения в браузере. После рендеринга компонента можно получить доступ к его дочерним элементам через объект nativeElement:

it('render title in h1 tag', async(() => {
   const fixture = TestBed.createComponent(AppComponent);
   fixture.detectChanges();
   const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-unit-test!');
}));

Unit-тестирование в Node JS

Node.js позволяет разработчикам выполнять код JavaScript на стороне сервера. Это платформа с открытым исходным кодом, которая интегрируется с популярными фреймворками, такими как Mocha, для тестирования JavaScript. Тестировщики могут указать, что внедряемый ими код является тестом, вставив ключевые слова Mocha для тестирования API.

Например, it() указывает, что код представляет собой один тест, в то время как describe() указывает на то, он содержит группу тест-кейсов. Внутри группы тестов describe() могут быть подгруппы. Каждая функция принимает два аргумента: описание, отображаемое в отчете о тестировании, и функцию обратного вызова.

Вот пример самого простого тест-сьюта с одним тест-кейсом:

const {describe} = require('mocha');
const assert = require('assert');

describe('Simple test suite:', function() {
    it('1 === 1 should be true', function() {
        assert(1 === 1);
    });
});

Результаты теста должны выглядеть следующим образом:

$ cd src/projects/IBM-Developer/Node.js/Course/Unit-9
$ ./node_modules/.bin/mocha test/example1.js

  Simple test suite:
    ✓ 1 === 1 should be true

  1 passing (5ms)

Mocha поддерживает любую библиотеку утверждений. В этом примере используется модуль Node assert (относительно менее выразительная библиотека). 

Unit-тестирование в React Native

React Native – это фреймворк с открытым исходным кодом для разработки мобильных приложений на основе JavaScript. В него встроен фреймворк тестирования Jest. Разработчики могут использовать Jest для обеспечения корректности своей кодовой базы на JavaScript.

Jest обычно предустанавливается в приложениях React Native в качестве готового решения для тестирования. Разработчик может открыть файл package.json и настроить предустановку Jest на React Native:

"scripts": {
            "test": "jest"
},
"jest": {
         "preset": "jest-react-native"
}

Если, например, в приложении есть функция сложения простых чисел, тестировщик может предугадать правильный результат. Это легко проверить, импортировав функцию сложения в файл теста. Отдельный файл, содержащий функцию суммы, может  называться ExampleSumTest.js:

const ExampleSum = require('./ExampleSum');

test('ExampleSum equals 3', () => {
      expect(ExampleSum(1, 2).toBe(3);
});

Ожидаемый вывод Jest должен выглядеть следующим образом:

PASS ./ExampleSumTest.js
✓ ExampleSum equals 3 (5ms)

Лучшие практики модульного тестирования

Ниже приведены лучшие практики, которые вы можете использовать, чтобы сделать ваше unit-тестирование более эффективным.

Пишите читаемые тесты

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

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

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

Пишите детерминированные тесты

Детерминированный тест всегда проходит (если нет проблем) или всегда проваливается (если проблемы есть) на одном и том же участке кода. Результат теста не должен меняться пока вы не измените свой код. Напротив, нестабильный тест – это тест, который может пройти или не пройти из-за различных условий, даже если код остается неизменным. 

Недетерминированные тесты, также известные как «flaky-тесты», неэффективны, потому что разработчики не могут им доверять. Они неэффективно сообщают об ошибках в тестируемом блоке и могут заставить разработчиков игнорировать результаты тестов (в том числе и стабильных). 

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

Unit-тесты должны быть автоматизированы

Убедитесь, что тесты выполняются в автоматическом режиме. Это можно делать ежедневно, ежечасно или в рамках процесса непрерывной интеграции (CI). Все члены команды должны иметь доступ к отчетам о тестировании и просматривать их. 

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

Не используйте несколько утверждений в одном unit-тесте

Чтобы  unit-тесты были эффективными и управляемыми, каждый тест должен содержать только один тест-кейс. То есть в тесте должно быть только одно утверждение. 

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

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

Перевод статьи «Unit Testing: Definition, Examples, and Critical Best Practices».

1 комментарий к “Unit-тестирование. Примеры и лучшие практики”

  1. Пингбэк: Шаблон тест-кейса с примерами

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *