Антипаттерны юнит-тестирования | OTUS
⚡ Подписка на курсы OTUS!
Интенсивная прокачка навыков для IT-специалистов!
Подробнее

Курсы

Программирование
iOS Developer. Professional Kotlin Backend Developer Flutter Mobile Developer Symfony Framework C++ Developer. Basic Unity Game Developer. Basic Java Developer. Professional
-35%
Highload Architect Unity Game Developer. Professional React.js Developer Специализация Java-разработчик
-25%
Алгоритмы и структуры данных
-16%
Scala-разработчик C# Developer. Professional
-23%
Разработчик голосовых ассистентов и чат-ботов Team Lead Архитектура и шаблоны проектирования NoSQL Web-разработчик на Python Golang Developer. Professional PostgreSQL Vue.js разработчик Супер-практикум по использованию и настройке GIT Разработчик IoT Подготовка к сертификации Oracle Java Programmer (OCAJP) Программист С HTML/CSS
Инфраструктура
Инфраструктурная платформа на основе Kubernetes Microservice Architecture Базы данных Highload Architect Reverse-Engineering. Professional
-8%
Network engineer. Basic Administrator Linux.Basic MongoDB Infrastructure as a code MS SQL Server Developer Cloud Solution Architecture Мониторинг и логирование: Zabbix, Prometheus, ELK Супер-практикум по использованию и настройке GIT Разработчик IoT Экcпресс-курс «ELK» Супер-интенсив "Tarantool" Экспресс-курс «CI/CD или Непрерывная поставка с Docker и Kubernetes» Экспресс-курс «Введение в непрерывную поставку на базе Docker»
Корпоративные курсы
Безопасность веб-приложений Экосистема Hadoop, Spark, Hive Пентест. Практика тестирования на проникновение Node.js Developer Java QA Engineer. Basic
-18%
Reverse-Engineering. Professional
-8%
DevOps практики и инструменты NoSQL Reverse-Engineering. Basic Cloud Solution Architecture Внедрение и работа в DevSecOps Супер-практикум по работе с протоколом BGP Game QA Engineer Супер - интенсив по Kubernetes Дизайн сетей ЦОД Экспресс-курс «IaC Ansible» Экспресс-курс по управлению миграциями (DBVC) Экспресс-курс "Версионирование и командная работа с помощью Git" Основы Windows Server
Специализации Курсы в разработке Подготовительные курсы Подписка
+7 499 938-92-02

Антипаттерны юнит-тестирования

Введение

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

Только позитивное тестирование

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

doctest positive

def inc(x):
    """
    >>> inc(3) # Инкремент 3 вернет 4
    4
    """
    return x + 1

import doctest
doctest.testmod()

pytest positive

def inc(x):
    return x + 1

def test_inc_three():
    " Инкремент 3 вернет 4 "
    assert inc(3) == 4

unittest positive

def inc(x):
    return x + 1

import unittest

class TestInc(unittest.TestCase):
    def test_three(self):
        " Инкремент 3 вернет 4 "
        self.assertEqual(inc(3), 4)

Возможные исключения тоже часть интерфейса и требует как тестирования, так и документации. Сделать это не сильно сложнее:

doctest negative

import math
def sqrt(x):
    """
    >>> sqrt(-1)
    Traceback (most recent call last):
        ...
    ValueError: Расчет квадратного корня возможен для чисел > 0
    """
    if x < 0:
        raise ValueError("Расчет квадратного корня возможен для чисел > 0")
    return math.sqrt(x)

import doctest

# Флаг ELIPSIS Позволяет писать ... вместо изменчивого стека исключения
doctest.testmod(optionflags=doctest.ELLIPSIS)

pytest negative

import pytest
def sqrt(x):
    ...  # код опущен

def test_sqrt_from_negative():
    " Исключение вместо квадратного корня из -1 "
    with pytest.raises(ValueError) as exc_info:
        sqrt(-1)
    # Pytest кладет объект исключения в поле value
    assert exc_info.value.args = ("Расчет квадратного корня возможен для чисел > 0", )

unittest negative

import unittest

def sqrt(x):
    ...  # код опущен

class TestSqrt(unittest.TestCase):
    def test_sqrt_from_negative(self):
        " Исключение вместо квадратного корня из -1 "
        with self.assertRaises(ValueError) as exc_info:
            sqrt(-1)
        # unittest кладет объект исключения в поле exception
        self.assertEqual(
            exc_info.exception.args,
            ("Расчет квадратного корня возможен для чисел > 0", )
        )

Медленные тесты

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

doctest skip

import math
def sqrt(x):
    """
    следующая строка не будет выполнена в тестах
    >>> sqrt(-1) # doctest: +SKIP
    """
    ... # код опущен

pytest skip

import pytest
def sqrt(x):
    ...  # код опущен

@pytest.mark.skip("Пока пропустим чтобы не тормозило")
def test_sqrt():
    ... # код опущен

unittest skip

import unittest

def sqrt(x):
    ...  # код опущен

class TestSqrt(unittest.TestCase):
    @unittest.skip("Пока пропустим чтобы не тормозило")
    def test_sqrt(self):
        ... # код опущен

envSkipIf

Конечно, хотелось бы, чтобы пропуском тестов можно было управлять, не меняя код. Примеры кода в doctest не получится оставить читаемыми при такой доработке. А вот у unittest и pytest есть декораторы skipIf и mark.skipif соответственно, которые пропускают кейс, если первым аргументом передан True. Их можно обернуть проверкой переменных окружения. Тогда управлять полнотой тестов можно будет не исправляя код, а просто устанавливая нужные переменные окружения перед запуском.

from os import environ

def skip_by_env(conditional_skipper):
    "Декоратор для пропуска тестов"
    def skip_wrapper(test_case):
        """
        Вызывает conditional_skipper c переданной функцией и признаком пропуска.
        """
        env_name = 'SKIP_CASE_' + func.__qualname__
        return conditional_skipper(
            env_name in environ
            f"Пропущен, так как обнаружена переменная окружения {env_name}"
        )(test_case)
    return skip_wrapper

@skip_by_env(pytest.mark.skipif) # или unittest.skipIf
def test_sqrt():
    ... # код опущен

Безымянные утверждения

Даже при удачном нейминге тестовых сценариев смысл отдельных утверждений (assert) ускользает от понимания при разборе лога упавших тестов. В таких случаях приходится вникать в код тестов. Также, если тестовые данные лежат в списке и перебираются циклом, утверждения, оторванные от данных, могут терять смысл. Решить такую проблему можно не одним способом. В pytest и для тестовых данных, и для идентификации сценариев применяются фикстуры, в unittest — субтесты. Но иногда добавить строчку, аннотирующее утверждение, будет быстрее.

doctest msg

def inc(x):
    """
    следующая строка не будет выполнена в тестах
    >>> for arg, etalon in (
    ...        (-1, 0),
    ...        (0, 1),
    ...        (3, 4),
    ...     ):
    ...     result = inc(arg)
    ...     assert etalon == result, f'Инкремент {arg} должен быть равен {etalon}, а не {result}'
    """
    return x + 1

pytest msg

def test_inc():
    for arg, etalon in (
            (-1, 0),
            (0, 1),
            (3, 4),
        ):
        result = inc(arg)
        assert result == etalon, f'Инкремент {arg} должен быть равен {etalon}, а не {result}'

unittest msg

import unittest

class TestInc(unittest.TestCase):
    def test_inc(self):
        for arg, etalon in (
                    (-1, 0),
                    (0, 1),
                    (3, 4),
                ):
            result = inc(arg)
            self.assertEqual(
                result,
                etalon,
                f'Инкремент {arg} должен быть равен {etalon}, а не {result}'
            )

Заключение

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

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться