Рубрики
Без рубрики

Модульное тестирование в Python с помощью модульного теста

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

Автор оригинала: Robley Gori.

Вступление

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

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

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

Модульное тестирование против других форм тестирования

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

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

Модульное тестирование подпадает под функциональное тестирование наряду с интеграционным тестированием и регрессионным тестированием .

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

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

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

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

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

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

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

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

Вдохновленный JUnit testing framework for Java , unit test – это тестовая платформа для программ Python, которая поставляется в комплекте с дистрибутивом Python начиная с Python 2.1. Иногда его называют PyUnit . Фреймворк поддерживает автоматизацию и агрегацию тестов, а также общий код настройки и завершения работы для них.

Он достигает этого и многого другого с помощью следующих концепций:

  • Test Fixture : Определяет подготовку, необходимую для выполнения тестов, и любые действия, которые необходимо выполнить после завершения теста. Приспособления могут включать в себя настройку и подключение базы данных, создание временных файлов или каталогов, а также последующую очистку или удаление файлов после завершения теста.
  • Тестовый случай : Относится к индивидуальному тесту, который проверяет наличие определенного ответа в данном сценарии с определенными входными данными.
  • Test Suite : Представляет собой совокупность тестовых случаев, которые связаны и должны выполняться вместе.
  • TestRunner : Координирует выполнение тестов и предоставляет результаты процесса тестирования пользователю через графический интерфейс пользователя, терминал или отчет, записанный в файл.

unit test – это не единственный тестовый фреймворк для Python, другие включают Pytest , Robot Framework , Lettuce для BDD и Behavior Framework .

Если вам интересно узнать больше о тестовой разработке на Python с помощью PyTest , мы вас накроем!

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

Мы собираемся исследовать фреймворк unittest , создав простое приложение-калькулятор и написав тесты, чтобы убедиться, что оно работает так, как ожидалось. Мы будем использовать процесс Test-Driven Development , начав с тестов, а затем реализовав функциональность, чтобы тесты прошли.

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

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

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

Наш калькулятор будет Простым калькулятором класса с функциями для обработки четырех операций, ожидаемых от него. Давайте начнем тестирование с написания тестов для операции сложения в нашем test_simple_calculator.py :

import unittest
from simple_calculator import SimpleCalculator

class AdditionTestSuite(unittest.TestCase):
    def setUp(self):
        """ Executed before every test case """
        self.calculator = SimpleCalculator()

    def tearDown(self):
        """ Executed after every test case """
        print("\ntearDown executing after the test case. Result:")

    def test_addition_two_integers(self):
        result = self.calculator.sum(5, 6)
        self.assertEqual(result, 11)

    def test_addition_integer_string(self):
        result = self.calculator.sum(5, "6")
        self.assertEqual(result, "ERROR")

    def test_addition_negative_integers(self):
        result = self.calculator.sum(-5, -6)
        self.assertEqual(result, -11)
        self.assertNotEqual(result, 11)

# Execute all the tests when the file is executed
if __name__ == "__main__":
    unittest.main()

Мы начинаем с импорта модуля unittest и создания набора тестов( Addition TestSuite ) для операции добавления.

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

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

Функции test_addition_two_integers() , test_addition_integer_string() и test_addition_negative_integers() являются нашими тестовыми случаями. Предполагается, что калькулятор сложит два положительных или отрицательных целых числа и вернет сумму. При представлении целого числа и строки наш калькулятор должен возвращать ошибку.

assertEqual() и assertNotEqual () – это функции, которые используются для проверки выходных данных нашего калькулятора. Функция assertEqual() проверяет, равны ли два предоставленных значения, в нашем случае мы ожидаем, что сумма 5 и 6 быть 11 , поэтому мы сравним это значение со значением, возвращаемым нашим калькулятором.

Если эти два значения равны, то тест пройден. Другие функции утверждения, предлагаемые unittest , включают:

  • assertTrue(a) : Проверяет, является ли предоставленное выражение истинным
  • assert Greater(a, b) : Проверяет, является ли a больше b
  • assertNotIn(a, b) : Проверяет, находится ли a в b
  • assertLessEqual(a, b) : Проверяет, является ли a меньше или равно b
  • и т.д…

Список этих утверждений можно найти в этой шпаргалке .

Когда мы выполняем тестовый файл, это вывод:

$ python3 test_simple_calulator.py

tearDown executing after the test case. Result:
E
tearDown executing after the test case. Result:
E
tearDown executing after the test case. Result:
E
======================================================================
ERROR: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 22, in test_addition_integer_string
    result = self.calculator.sum(5, "6")
AttributeError: 'SimpleCalculator' object has no attribute 'sum'

======================================================================
ERROR: test_addition_negative_integers (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 26, in test_addition_negative_integers
    result = self.calculator.sum(-5, -6)
AttributeError: 'SimpleCalculator' object has no attribute 'sum'

======================================================================
ERROR: test_addition_two_integers (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 18, in test_addition_two_integers
    result = self.calculator.sum(5, 6)
AttributeError: 'SimpleCalculator' object has no attribute 'sum'

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (errors=3)

В верхней части вывода мы видим выполнение функции tearDown() через печать указанного нами сообщения. Далее следует буква E и сообщения об ошибках, возникающие при выполнении наших тестов.

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

  • Полная остановка ( . ) : Указывает на прохождение теста
  • Буква “F” : Указывает на неудачный тест
  • Буква “Е” : Указывает на ошибку, возникшую во время выполнения теста

В нашем случае мы видим букву E , означающую , что наши тесты столкнулись с ошибками, возникшими при выполнении наших тестов. Мы получаем ошибки, потому что мы еще не реализовали дополнение функциональность нашего калькулятора:

class SimpleCalculator:
    def sum(self, a, b):
        """ Function to add two integers """
        return a + b

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

$ python3 test_simple_calulator.py
E..
======================================================================
ERROR: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 22, in test_addition_integer_string
    result = self.calculator.sum(5, "6")
  File "/Users/robley/Desktop/code/python/unittest_demo/src/simple_calculator.py", line 7, in sum
    return a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (errors=1)

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

    def sum(self, a, b):
        if isinstance(a, int) and isinstance(b, int):
            return a + b

Когда мы проведем наши тесты еще раз:

$ python3 test_simple_calulator.py
F..
======================================================================
FAIL: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 23, in test_addition_integer_string
    self.assertEqual(result, "ERROR")
AssertionError: None != 'ERROR'

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

На этот раз наша функция sum выполняется до завершения, но наш тест терпит неудачу. Это происходит потому, что мы не возвращали никакого значения, когда один из входных данных не является целым числом. Наше утверждение сравнивает None с ERROR , и поскольку они не равны, тест не выполняется. Чтобы сделать наш тест проходным, мы должны вернуть ошибку в нашей функции sum() :

def sum(self, a, b):
    if isinstance(a, int) and isinstance(b, int):
        return a + b
    else:
        return "ERROR"

И когда мы проводим наши тесты:

$ python3 test_simple_calulator.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

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

Мы также можем проверить, возникает ли исключение. Например, когда число делится на ноль, возникает исключение ZeroDivisionError . В нашем Division TestSuite мы можем подтвердить , было ли вызвано исключение:

class DivisionTestSuite(unittest.TestCase):
    def setUp(self):
        """ Executed before every test case """
        self.calculator = SimpleCalculator()

    def test_divide_by_zero_exception(self):
        with self.assertRaises(ZeroDivisionError):
            self.calculator.divide(10, 0)

Функция test_divide_by_zero_exception() выполнит функцию divide(10, 0) нашего калькулятора и подтвердит, что исключение действительно было вызвано. Мы можем выполнить Division TestSuite изолированно, следующим образом:

$ python3 -m unittest test_simple_calulator.DivisionTestSuite.test_divide_by_zero_exception
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

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

Вывод

В этой статье мы исследовали фреймворк unittest и определили ситуации, в которых он используется при разработке программ на Python. Фреймворк unit test , также известный как PyUnit , поставляется с дистрибутивом Python по умолчанию, в отличие от других фреймворков тестирования. В TDD-манере мы написали тесты для простого калькулятора, выполнили тесты, а затем реализовали функциональность, чтобы сделать тесты проходными.

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

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