Юнит-тесты для новичков: зачем нужны и как писать

Разбираемся с базой и даём 10 советов для идеального кода, покрытого юнит-тестами.

385 открытий2К показов
Юнит-тесты для новичков: зачем нужны и как писать

Заметили, что даже в вакансиях для джунов всё чаще требуют разбираться в тестировании? Рассказываем, что такое юнит-тесты, почему они важны и как писать их быстро и без ошибок.

Зачем тестировать код

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

Тесты обеспечивают:

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

Поэтому тесты экономят не только время разработчика, но и ресурсы компании. А ещё повышают доверие пользователей к продукту.

Что такое юнит-тесты

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

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

Она состоит из трёх частей:

  1. Юнит-тесты — основа пирамиды
  2. Интеграционные тесты — средний уровень
  3. Системные тесты — самый высокий и узкий уровень пирамиды

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

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

За что любят юнит-тесты

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

Какие виды юнит-тестов бывают

Существует несколько видов юнит-тестов, которые помогают проверить различные аспекты кода:

  1. позитивное тестирование — проверяет, правильно ли функция или метод работает при корректных входных данных
  2. негативное тестирование — оценивает, как код реагирует на нестандартные ситуации или введение очевидно некорректных данных. Например, если написать «-15» в поле для ввода суммы, которую нужно перевести на другой счёт, или пытаться делить на 0
  3. граничное тестирование — проверка граничных значений входных данных. Например, если функция принимает числа от 1 до 100, граничное тестирование будет проверять значения 0, 1, 100 и 101
  4. тестирование с использованием моков — моками называют фейковые базы данных, серверы и другие сложные системы. Это «дублёры», с помощью которых можно безопасно протестировать систему. Например, если метод обращается к базе данных, используют мок для имитации ответа базы данных — без подключения к реальной базе
  5. тестирование состояния — проверка, что функция или метод приводит систему или объект в ожидаемое состояние после выполнения. Например, есть метод, который добавляет элемент в список, — после этого размер списка должен увеличиться на один

На практике можно использовать как один вид юнит-тестов, так и сразу все — это зависит от сложности проекта и требований к тестированию.

Как написать юнит-тест

Подготовка юнит-теста начинается с понимания, что именно нужно проверить, и заканчивается написанием нужного кода.

Вот как выглядит этот процесс по шагам:

  1. Определить, какую функцию или метод нужно проверить. Уточните, какое поведение или результат вы ожидаете от этой функции или метода.
  2. Понять, требуется ли изолированное тестовое окружение — например, если код взаимодействует с базой данных или внешним сервисом. Имитировать эти зависимости помогут заглушки и моки.
  3. Написать код теста. Можно использовать специальный фреймворк на выбранном вами языке программирования.
  4. Запустить тест. В тестовых фреймворках обычно есть специальные команды, которые помогают запустить тест и увидеть результат. Если тест не прошёл, нужно проанализировать и исправить ошибки.
  5. Провести рефакторинг и оптимизацию. Если нужно, удалите лишний код и улучшите структуру.
  6. Повторить процесс. Для каждой новой функции, метода или аспекта поведения существующего кода нужен новый юнит-тест.

Разберём код на примере.

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

Вот код класса InternetBanking на Python. В нём предусмотрено исключение для случаев, когда при переводе вводят отрицательную сумму или когда человек пытается снять больше денег, чем есть на счету.

			class BankAccount:
    def __init__(self, initial_balance=0):
        self.balance = initial_balance

    def deposit(self, amount):
        if amount < 0:
            raise ValueError("Amount should be positive!")
        self.balance += amount
        return self.balance

    def withdraw(self, amount):
        if amount < 0:
            raise ValueError("Amount should be positive!")
        if amount > self.balance:
            raise ValueError("Insufficient funds!")
        self.balance -= amount
        return self.balance

    def check_balance(self):
        return self.balance

		

А так может выглядеть юнит-тест для InternetBanking, который включает в себя позитивное, негативное и тестирование состояния, чтобы покрыть разные аспекты функциональности:

			import unittest

class TestBankAccount(unittest.TestCase):

    def setUp(self):
        # Создаём объект BankAccount перед каждым тестом
        self.account = BankAccount(100) # начальный баланс = 100

    def test_deposit(self):
        # Проверяем функцию депозита
        self.assertEqual(self.account.deposit(50), 150)

    def test_withdraw_valid(self):
        # Проверяем допустимое снятие
        self.assertEqual(self.account.withdraw(50), 50)

    def test_withdraw_insufficient_funds(self):
        # Проверяем снятие с недостаточным балансом
        with self.assertRaises(ValueError) as context:
            self.account.withdraw(150)
        self.assertEqual(str(context.exception), "Insufficient funds!")

    def test_withdraw_negative_amount(self):
        # Проверяем снятие отрицательной суммы
        with self.assertRaises(ValueError) as context:
            self.account.withdraw(-50)
        self.assertEqual(str(context.exception), "Amount should be positive!")

    def test_check_balance(self):
        # Проверяем баланс
        self.assertEqual(self.account.check_balance(), 100)

		

Пояснение по коду:

  • класс BankAccount содержит три метода: deposit, withdraw и check_balance
  • в юнит-тестах мы используем модуль unittest для создания тестового класса TestBankAccount
  • метод setUp вызывается перед каждым тестом и создаёт новый объект BankAccount с начальным балансом 100
  • далее идут тестовые методы, которые проверяют различные аспекты работы класса BankAccount: депозит, снятие с допустимым балансом, снятие с недостаточным балансом, снятие отрицательной суммы и проверка баланса

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

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

Как покрыть код юнит-тестами: 10 советов для начинающих

Часто новичкам-разработчикам кажется, что покрыть весь код тестами — сложно и долго. Собрали советы, которые помогут быстро и качественно справиться с этой задачей.

1. Сначала — понять задачу
Разрабатываете новый функционал или сервис? Разберитесь, что именно он должен делать. Например, зарплатный модуль может не только считать зарплату, но и вычитать налоги, создавать отчёты, добавлять премии.

2. Принцип «новый код — новый тест»
Если добавляете новую функцию для автоматического расчёта налогов из зарплаты, напишите тест прежде, чем интегрировать функцию в основной код. Это сократит количество ошибок и ускорит процесс разработки.

3. Строгий контроль качества
В некоторых командах есть правило: новый код без юнит-теста не проходит код-ревью. Это гарантирует, что каждый фрагмент кода проверен и работает корректно.

4. Готовые инструменты
Не изобретайте велосипед, используйте готовые фреймворки. Например, для Python это может быть pytest или unittest.

5. Простота — залог успеха
Разбивайте сложную функцию на несколько небольших — это упростит процесс тестирования и облегчит чтение кода.

6. Негативные тесты тоже важны
Помимо проверки корректности работы кода, проверьте его устойчивость к ошибкам. Например, что произойдёт, если в модуль учёта зарплаты введут отрицательное значение.

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

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

9. Скорость тестирования
Юнит-тесты должны выполняться быстро. Если вы заметили, что некоторые тесты занимают слишком много времени, возможно, стоит их оптимизировать. Например, разбивать на более короткие и простые части.

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

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

Если вы уже знакомы с темой и успешно пишете юнит-тесты, расскажите, что ещё важно учесть новичкам.

385 открытий2К показов