Skip to content

Latest commit

 

History

History
707 lines (482 loc) · 42.2 KB

lesson15.md

File metadata and controls

707 lines (482 loc) · 42.2 KB

Лекция 15. Тестирование

Общая информация

Тестирование - это огромная, нет ОГРОМНАЯ тема, настолько огромная, что порождает несколько отдельных видов сотрудников в IT индустрии.

Исследование против плана

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

Исследовательское тестирование — это форма тестирования, которая проводится без плана. В таком виде тестирования вы просто изучаете приложение.

Чтобы получить полный набор ручных тестов, необходимо выполнить следующие шаги:

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

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

Это не особо прикольно?

Вот где приходит на помощь тест план.

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

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

Виды тестирования

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

Но давайте поверхностно посмотрим на вот эту таблицу:

Вот текст с описанием каждого вида тестирования, представленного в схеме:

  1. По доступности кода:

    • Черного ящика (Black Box testing): Тестирование проводится без знания внутренней структуры системы. Проверяются функциональные требования.
    • Серого ящика (Grey Box testing): Тестирование проводится с частичным знанием внутренней структуры системы. Проверяются и функциональные, и некоторые нефункциональные требования.
    • Белого ящика (White Box testing): Тестирование проводится с полным знанием внутренней структуры системы. Проверяются внутренние механизмы и логика работы системы.
  2. По объекту (предмету) тестирования:

    • Интерфейса пользователя (UI testing): Проверка удобства и правильности работы пользовательского интерфейса.
    • Локализации (Localization testing): Проверка правильности адаптации ПО для различных языков и регионов.
    • Скорости и надежности (Speed/Stress/Performance testing): Проверка производительности и устойчивости системы под нагрузкой.
    • Безопасности (Security testing): Проверка системы на уязвимости и защиты от различных видов атак.
    • Удобства использования пользователем (Usability/UX testing): Проверка удобства и интуитивности использования системы для конечных пользователей.
    • Совместимости (Compatibility testing): Проверка корректности работы системы в различных средах (операционные системы, браузеры и т.д.).
    • Функциональное (Functional testing): Проверка корректности выполнения всех заявленных функций системы.
  3. По позитивности сценариев:

    • Позитивное (Positive testing): Проверка системы на корректное поведение при введении ожидаемых данных.
    • Негативное (Negative testing): Проверка системы на устойчивость к введению неожиданных или некорректных данных.
  4. По времени проведения:

    • Альфа (Alpha testing): Внутреннее тестирование, проводимое разработчиками или тестировщиками внутри компании.
    • Бета (Beta testing): Внешнее тестирование, проводимое потенциальными или реальными пользователями.
    • Дымовое (Smoke test): Быстрая проверка основных функций системы для выявления грубых ошибок.
    • Санитарное или Соответствия (Sanity/Confidence test): Проверка работоспособности после внесения небольших изменений.
    • Новых функциональностей (New feature test): Тестирование вновь добавленных функций.
    • Регрессионное (Regression test): Проверка существующего функционала после внесения изменений в систему.
    • Приемочное (Acceptance or Certification test): Финальная проверка системы перед передачей заказчику или выпуском в продакшн.
  5. По уровням тестирования:

    • Компонентное (Component testing): Проверка отдельных компонентов или модулей системы.
    • Интеграционное (Integration testing): Проверка взаимодействия между компонентами системы.
    • Системное (System or End to end testing): Проверка всей системы целиком на соответствие требованиям.
  6. По автоматизированности:

    • Ручное (Component testing): Тестирование, проводимое вручную тестировщиком.
    • Автоматизированное (Automation testing): Тестирование с использованием автоматизированных скриптов и инструментов.
    • Полуавтоматизированное (Semi automated testing): Сочетание ручного и автоматизированного тестирования.
  7. По степени подготовки:

    • По документации (Formal/Documented testing): Тестирование, проводимое по заранее подготовленным сценариям и планам.
    • Интуитивное (Ad hoc testing): Спонтанное тестирование без заранее подготовленных планов и сценариев.
  8. По субъекту:

    • Альфа-тестировщик штатный (Alfa-tester): Внутренний сотрудник компании, занимающийся тестированием.
    • Бета-тестировщик внешний (Beta-tester): Внешний пользователь, привлекаемый для тестирования системы.

Так вот. Например, performance и security(penetration) тестирования выполняют отдельные специалисты (ну если они есть).

И если вы думаете, что тестирование, это для тестировщиков, а мы тут вроде на программистов учимся. То у меня для вас плохие новости. Очень весомую часть своего рабочего времени программист тратит на тестирование!

Уровни тестирования

Подумайте, как вы можете проверить свет фар в автомобиле. Вам нужно запустить фары, после чего выйти из машины и посмотреть горят ли фары. Проверка на то что фары вообще горят, называется assertion (асершен), но на самом деле вариантов что можно проверить невероятно много.

Существует 4 основных уровня тестирования функционала.

Модульные тесты

Модульные тесты (Unit Tests) - это тесты, проверяющие функционал конкретного модуля минимального размера. Если вы написали функцию или метод, то юнит тестом будет попытка вызвать этот метод с разными входными данными, и посмотреть на то, что вернёт результат. Пример из модуля. Функция для создания задачи. Передать туда все возможные варианты входных данных, и убедиться что, что-бы мы туда не передали, функция всегда ведет себя как ожидается.

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

В вашем написанном коде все точно так же. Почти любое действие это совокупность какого-то количества функций/методов итд.

Unit-тестирование - это тестирование каждого отдельного минимально возможного компонента.

Интеграционные тесты

Интеграционные тесты (Integration Tests) - это вид тестирования, когда проверяется целостность работы системы, без сторонних средств. Пример из вашего модуля. Вы пишете тест, который запускает полный набор действий для создания заметки. И в качестве проверки должно быть создание файла, в котором появилась новая задача.

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

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

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

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

Когда мы доберемся до веба, я покажу специальные средства (например, Selenium) который за нас будет открывать браузер, искать необходимые элементы на странице, имитировать ввод данных, нажатие кнопок, переход по ссылкам и т. д.

На данном этапе нас эти тесты не интересуют (Честно говоря, и потом не сильно будут интересовать)

Ручные тесты (Manual Tests) - вид тестов, когда мы полностью повторяем потенциальные действия пользователя.

Если вы хоть раз запускали свой код. Значит вы проводили ручное исследовательское тестирование, только об этом не знали.

Помните, нельзя закончит с тестами, можно только перестать!!

Как это вообще работает?

Теоретически можно написать рабочий проект вообще без единого теста (ваш модуль тому пример). Но чем больше и сложнее система, тем дороже стоимость ошибки или объем затраченного времени на поиск причины этой ошибки.

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

Кто все это пишет?

Люди в тестировании

Из тех с кем вы реально можете столкнуться это:

  • Другие разработчики (Они же dev- developer, SE - software engineer)
  • Автоматизированные тестировщики (Они же AQA - automation quality assurance, SDET - software development engineer in test)
  • Мануальные тестировщики (Тут без терминов, максимум мануальщики)

В целом любых тестировщиков называют QA(quality assurance) или иногда QC(quality control)

Зачем нам все эти люди

Кто пишет юнит тесты?

Юнит тесты - это тесты конкретно написанной функции или метода. А значит, что знание о том, как это работает, есть только у разработчика, а значит, их пишет разработчик.

  • Идеальный мир - разработчик покрывает всё тестами.

  • Реальность - разработчик покрывает основной функционал и тонкие места тестами.

  • Худший случай - юнит тестов нет, что приводит к усложнению написания и модификации проекта в несколько раз.

Кто пишет интеграционные тесты?

  • Идеальный мир - автоматизированные тестировщики, причём вполне возможно, что на другом языке программирования, связь вообще не обязательна.

  • Реальность - автоматизаторы, если они есть, разработчики, если автоматизаторов нет. Но даже если автоматизаторы есть бывают интеграционные тесты которые нужны вам самим как разработчикам, тогда они не причем, и этим занимаемся тоже мы.

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

Кто пишет приёмочные тесты?

  • Идеальный мир - всё те же автоматизаторы.

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

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

Кто выполняет ручные тесты?

  • Идеальный мир - мануальные тестировщики.

  • Реальность - если есть мануальные тестировщики, то они, если нет, то автоматизаторы, если и их нет, то разработчики в процессе разработки.

  • Худший случай - не проводятся, уверенность, что функционал работает, равна нулю.

Нельзя закончить писать тесты, можно только перестать

assert

Ключевое слово assert является основным инструментом для тестирования. Как это работает? По сути assert это только надстройка надо другой конструкцией над raise:

После assert нужно указать какое-то выражение или через запятую передать выражение и текст ошибки.

Если выражение после преобразования в булеан является истинным (True), то код продолжает работать дальше, а если нет ( False), то останавливает выполнение и рейзит ошибку, если она описана, то с текстом, если нет, то без.

Давайте глянем на примеры:

assert 1  # Все хорошо, этот код, отработает и перейдет на следующую строчку
assert 0  # Мы увидим ошибку
assert 4 == 5, "4 in not equal to 5"  # Мы увидим ошибку которую сами и написали

по сути весь код сверху, это просто обертка над вот такой конструкцией

condition = 4 == 5
message = "4 in not equal to 5"
if not condition:
    raise AssertionError(message)

Но так в реальности никто не пишет!

TestCase

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

Например, в вашем модуле, есть создание, изменение и удаление задачи. Это три разных теста, которые можно объединить в один тест кейс. Обработка задач.

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

Тестовые фреймворки и что это вообще такое

Фреймворк

Фреймворк (framework) — это структурированная платформа для разработки программного обеспечения, предоставляющая набор готовых компонентов, инструментов и библиотек, которые упрощают и ускоряют процесс разработки. Фреймворки обеспечивают стандартный способ создания и организации кода, что способствует повышению эффективности и качества разработки.

Если простыми словами, то это конструктор (как лего), из готовых элементов, которые остается только собрать, не нужно каждую детальку вытачивать по отдельности

Какие бывают тестовые фреймворки для python

Технически их довольно много, вот основные:

  1. unittest:

    • Описание: Встроенный в стандартную библиотеку Python, unittest предоставляет базовый функционал для создания и выполнения тестов.
    • Особенности: Поддержка организации тестов в тестовые наборы (test suites), классы и методы для создания тестов, средства для проверок (assertions).
  2. pytest:

    • Описание: Один из самых популярных фреймворков для тестирования в Python, благодаря своей простоте и расширяемости.
    • Особенности: Простота использования, поддержка фикстур (fixtures), мощная система плагинов, хорошая интеграция с другими инструментами.
  3. nose2:

    • Описание: Продолжение проекта nose, который больше не поддерживается. nose2 сохраняет философию nose, обеспечивая автоматическое обнаружение тестов и совместимость с unittest.
    • Особенности: Простота настройки, поддержка плагинов, совместимость с тестами, написанными для unittest.
  4. doctest:

    • Описание: Фреймворк, встроенный в стандартную библиотеку Python, который позволяет писать тесты прямо в документационных строках (docstrings).
    • Особенности: Простой способ проверки корректности примеров кода в документации, минимальная настройка.
  5. Hypothesis:

    • Описание: Библиотека для тестирования на основе свойств (property-based testing), которая генерирует случайные входные данные для ваших тестов.
    • Особенности: Генерация широкого спектра тестовых случаев, автоматическое обнаружение граничных условий, интеграция с unittest и pytest.
  6. tox:

    • Описание: Инструмент для автоматизации тестирования в разных средах.
    • Особенности: Поддержка нескольких сред выполнения, интеграция с различными системами сборки и CI/CD, управление зависимостями.
  7. robot framework:

    • Описание: Фреймворк для тестирования на уровне системы с использованием тестовых сценариев, написанных на естественном языке.
    • Особенности: Поддержка ключевых слов (keyword-driven testing), возможность расширения с помощью библиотек Python, хорошие возможности для создания отчетов.

Но так как пока что мы не умеем работать с устанавливаемыми модулями (научимся через 5 занятий). То наш выбор падает на unittest. Он входит в стандартную библиотеку питона, и покрывает весь необходимый нам функционал.

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

К реальности

Тесты практически всегда пишут в виде тест кейсов, причём разделяя модульные и интеграционные (иногда еще ацептанс).

Для запуска тестов используются так называемые test runner - это специальное приложение, которое умеет искать и запускать тесты.

Мы будем пользоваться таким для unittest который уже встроен в python.

Остальные действуют по тем же принципам с немного другим синтаксисом. Так что если увидите другие тестовые фреймворки, не пугайтесь, они работают практически так же.

Во встроенном в Python модуле unittest есть класс TestCase, все тесты должны быть описаны в его наследниках и название каждого метода должно начинаться с test.

Встроенные assert

Вместо обычного assert юниттест использует свои сразу заготовленные методы, вот некоторые из них:

.assertEqual(a, b)  # a == b

.assertTrue(x)  # bool(x) is True

.assertFalse(x)  # bool(x) is False

.assertIs(a, b)  # a is b

.assertIsNone(x)  # x is None

.assertIn(a, b)  # a in b

.assertIsInstance(a, b)  # isinstance(a, b)

Простейший пример

В корне нашего проекта создадим файл tests.py

# tests.py
import unittest


class TestSum(unittest.TestCase):
    def test_sum(self):
        self.assertEqual(sum([1, 2, 3]), 6, "Should be 6")

    def test_sum_tuple(self):
        self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")


if __name__ == '__main__':
    unittest.main()

Обратите внимание, код заканчивается такими строками:

if __name__ == '__main__':
    unittest.main()

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

Теперь если мы запустим файл, в котором это написано, мы увидим следующее:

$ python tests.py
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_sum_unittest.py", line 9, in test_sum_tuple
    self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)

Один тест успешен, и один упал.

Запуск и поиск тестов

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

python -m unittest

Если запустить ее вообще без параметров, то она будет искать ВСЕ файлы и папки, которые начинаются со слова test и попытается их запустить.

Файлы внутри папок будут найдены, только если в папке есть файл __init__.py, иначе фреймворк просто их не найдет

Или указать конкретный файл:

python -m unittest tests.py

Если файл находится в папке то вот так:

python -m unittest folder/tests.py

Так же можно запустить конкретный тесткейс или даже отдельный тест

python -m unittest tests.TestSum
python -m unittest tests.TestSum.test_sum

Запустить можно как все целиком, так и любыми необходимыми частями

Методы setUp и tearDown

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

Тесты в python переехали из Java. И это причина почему названия этих методов не соответствуют PEP8. Просто смирились, запомнили и пользуемся.

import unittest


class TestSum(unittest.TestCase):

    def setUp(self):
        self.my_num = 5

    def test_odd(self):
        self.assertTrue(self.my_num % 2, "Number is odd")

    def tearDown(self):
        self.my_num += 1

Пропуск тестов

В пакете unittest есть декораторы skip, skipIf и skipUnless.

Необходимы для пропуска ненужных на данном этапе тестов.

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

class MyTestCase(unittest.TestCase):

    @unittest.skip("demonstrating skipping")
    def test_nothing(self):
        self.fail("Shouldn't happen")

    @unittest.skipIf(mylib.__version__ < (1, 3),
                     "Not supported in this library version")
    def test_format(self):
        # Tests that work for only a certain version of the library.
        pass

    @unittest.skipUnless(sys.platform.startswith("win"), "Requires Windows")
    def test_windows_support(self):
        # windows specific testing code
        pass

Mock

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

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

Mock является частью стандартной библиотеки начиная с python 3.3, очень вряд ли вы столкнетесь с такими старыми версиями в современном мире

Как это работает?

Можно создать Mock объект и заменить им всё что угодно. Мы можем назначить ему возвращаемый результат, для вызова чего угодно. Таким образом это объект который не вызывает ошибку при любом его использовании и можно в нем настроить любые атрибуты или методы

from unittest.mock import Mock

mock = Mock()
mock.some_attribute  # все ок, не существующий атрибут существует
mock.any_method()  # опять все ок, и все существует
mock.method1().attr1.attr2.method2()  # так тоже все ок, любой атрибут или метод будет возвращать Mock объект

Мы можем использовать фейковый объект в качестве аргумента или целиком заменяя сущность:

# Pass mock as an argument to do_something()
do_something(mock)

# Patch the random library
random = mock

Есть достаточно много способов использовать Mock, очень хорошая статья Тут

Рассмотрим основные

Контроль возвращаемого результата

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

Для упрощения пока засунем все в один файл:

from datetime import datetime


def is_weekday():
    today = datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return 0 <= today.weekday() < 5


# Test if today is a weekday
assert is_weekday()

Если мы запустим этот тест в воскресенье, то мы получим exception, что же с этим делать? Замокать... Mock объект может возвращать по вызову любой функции необходимое нам значение посредством заполнения return_value.

import datetime
from unittest.mock import Mock

# Save a couple of test days
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)

# Mock datetime to control today's date
datetime = Mock()


def is_weekday():
    today = datetime.datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return 0 <= today.weekday() < 5


# Mock .today() to return Tuesday
datetime.datetime.today.return_value = tuesday
# Test Tuesday is a weekday
assert is_weekday()
# Mock .today() to return Saturday
datetime.datetime.today.return_value = saturday
# Test Saturday is not a weekday
assert not is_weekday()

В этом примере, мы заставили библиотеку datetime не возвращать реальные результаты, а возвращать то, что нужно нам.

Детально изучите этот пример!

Если нам необходимо, чтобы после повторного вызова мы получали другие результаты, то нам поможет side_effect. Работает также, как и return_value, только принимает перебираемый объект и с каждым вызовом возвращает следующее значение.

mock_poll = Mock(side_effect=[None, 'data'])
mock_poll()
# None
mock_poll()
# 'data'

Или как в прошлом примере:

import datetime
from unittest.mock import Mock

# Save a couple of test days
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)

# Mock datetime to control today's date
datetime = Mock()


def is_weekday():
    today = datetime.datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return 0 <= today.weekday() < 5


# Mock .today() to return Tuesday first time and Saturday second time
datetime.datetime.today.side_effect = [tuesday, saturday]
assert is_weekday()
assert not is_weekday()

Декоратор patch

Допустим, у нас есть класс, где мы вызываем модуль random (как будет в вашем модуле), но нам для тестов не подходит случайность:

import random


class Randomizer:
    def value_from_list(self, some_list: list[int]) -> str:
        return f"{random.choice(some_list)}!"

Этот код будет возвращать случайное значение из списка.

И тест к этой функции:

from unittest import TestCase
from main import Randomizer


class TestCalculator(TestCase):
    def setUp(self):
        self.rand = Randomizer()

    def test_sum(self):
        answer = self.rand.value_from_list([1, 2, 3, 4])
        self.assertEqual(answer, "?!")  # С чем будем сравнивать? мы не знаем какое значение нам вернется

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

Как протестировать наш метод? Замокать случайность.

from unittest import TestCase
from unittest.mock import patch
from main import Randomizer


class TestCalculator(TestCase):
    def setUp(self):
        self.rand = Randomizer()
        self.values = [1, 2, 3, 4]

    @patch('main.Randomizer.random.choice')
    def test_sum(self, choice_mock):
        choice_mock.return_value = 1
        result = self.rand.value_from_list(self.values)
        self.assertEquals(result, "1!")

Пропатченные методы попадают в аргументы метода теста.

Практика

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

В идеале, вы сами должны покрыть свой модуль тестами целиком!