Как пишутся автотесты на питоне

Продолжаем погружаться в работу тестировщика, он же — QA, quality assurance engineer. Его задача — проверить код на наличие ошибок и работу программы в разных условиях. 

Мы уже писали о том, что ещё делают тестировщики и какие инструменты для этого используют:

  • Кто такой инженер по тестированию и стоит ли на него учиться
  • Зарплата 113 тысяч за то, чтобы ломать программы
  • Тестируем и исправляем калькулятор на JavaScript
  • Словарь тестировщика: автотесты, юнит-тесты и другие важные слова
  • Какой софт нужен, чтобы стать тестировщиком

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

Что такое автотесты

Автотесты — это когда одна программа проверяет работу другой программы. Работает это примерно так:

  1. У нас есть код программы с нужными функциями.
  2. Мы пишем новую программу, которая вызывает наши функции и смотрит на результат.
  3. Если результат совпадает с тем, что должно быть, — тест считается пройденным.
  4. Если результат не совпадает — тест не пройден и нужно разбираться.

Чтобы всё было наглядно, покажем работу автотестов на реальном коде.

Исходная программа

Допустим, мы пишем интерактивную текстовую игру — в ней всё оформляется текстом, и развитие игры зависит от ответов пользователя. Мы сделали отдельный модуль, который делает четыре вещи:

  • получает имя игрока;
  • принудительно делает в имени большую букву (вдруг кто-то случайно ввёл  с маленькой);
  • добавляет к нему приветствие;
  • сформированную строку отправляет как результат работы функции.
# Собираем приветствие
def hello(name):
    # делаем первую букву имени большой
    out = name.title()
    # формируем приветствие
    out = 'Привет, ' + out + '.'
    # возвращаем его как результат работы функции
    return out

Эта функция хранится в файле hello_function.py — так мы разбиваем программу на модули, каждый из которых делает что-то своё. 

Напишем начало основной программы, которая запрашивает имя, формирует приветствие и добавляет к нему стартовую фразу:

# импортируем функцию из другого файла
from hello_function import hello
# объясняем, что нужно сделать пользователю
print("Введите имя, чтобы начать игру")

# спрашиваем имя
name = input("Как вас зовут: ")
# обрабатываем имя и формируем приветствие
result = hello(name)
# добавляем вторую строку
print(result + " nДобро пожаловать в «Код»!")

Сохраним это в новом файле start.py и запустим его:

Делаем простые автотесты на Python

Вроде работает, но хорошо бы проверить, а всегда ли приветствие будет формироваться правильно? Можно сделать вручную, а можно написать автотест.

Пишем автотест

Первое, что нам нужно сделать, — подключить стандартный модуль для автотестов unittest. Есть модули покруче, но для наших проектов стандартного хватит с запасом. Также получаем доступ к функции hello() из файла hello_function.py — работу именно этой функции мы будем проверять автотестом.

# подключаем модуль для автотестов
import unittest
# импортируем функцию из другого файла
from hello_function import hello

А теперь самое важное: нам нужно объявить класс и функцию, внутри которой и будет находиться наш тест. Причём название функции должно начинаться с test_, чтобы она выполнялась автоматически.

Внутри функции делаем такое:

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

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

Для запуска тестов добавляем в конец кода стандартный вызов. Читайте комментарии, чтобы лучше вникнуть в код:

# подключаем модуль для автотестов
import unittest
# импортируем функцию из другого файла
from hello_function import hello

# объявляем класс с тестом
class HelloTestCase(unittest.TestCase):
    # функция, которая проверит, как формируется приветствие
   def test_hello(self):
        # отправляем тестовую строку в функцию
        result = hello("миша")
        # задаём ожидаемый результат
        self.assertEqual(result, "Привет, Миша.")

# запускаем тестирование
if __name__ == '__main__':
    unittest.main() 

После запуска мы увидим такое. Ответ «OK» означает, что наш тест сработал и завершился без ошибок:

Делаем простые автотесты на Python

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

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

Делаем простые автотесты на Python

Тест не пройден, а автотестер даже указал в чём

Что дальше

Мы написали самый простой тест и всего с одним условием. При этом мы не проверили тестами работу основной программы — в реальном проекте это тоже нужно было бы сделать. Чтобы попрактиковаться, мы сделаем тесты для одного из наших старых проектов на Python. Заодно проверим, нет ли там каких ошибок, которые мы не заметили.

Вёрстка:

Кирилл Климентьев

Мы будем использовать Selenium совместно с Python версий 3.x.x. Цель статьи – не дать фундаментальные знания по теории программирования и написания автотестов, а заинтересовать в этой области и показать, как они пишутся в целом.

1. Установка необходимых компонентов

Для начала работы нам потребуется установить Python на рабочую машину.

Переходим на официальный сайт Python и качаем установщик для вашей ОС (мы будем использовать Windows). В процессе инсталляции поставьте галочки на добавлении компонентов в системные переменные PATH. Дождитесь завершения процесса, и если программа попросит перезагрузки, перезагрузитесь. Если у вас Linux, интерпретатор может уже присутствовать в системе, в противном случае стоит установить его из репозитория пакетов вашего дистрибутива.

Проверьте корректность установки, перейдите в терминал (в Windows нажмите Win+R и запустите cmd или Alt+Ctrl+T в графической среде Linux). Выполните следующую команду:

        python --version
    

<i>Рис. 1. Должна быть выведена версия, а если что-то не получилось, проверьте выполнение по шагам и повторите попытку</i>

Рис. 1. Должна быть выведена версия, а если что-то не получилось, проверьте выполнение по шагам и повторите попытку

Далее нам понадобится сам Selenium:

        pip install selenium
    

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

        pip install pytest
    

Для создания приложений нужна интегрированная среда разработки или IDE (integrated development environment), но можно писать код и в обычном текстовом редакторе. Я выбрал самую популярную и удобную среду PyCharm от компании JetBrains.

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

2. Первый скрипт с использованием драйвера

Все компоненты готовы, давайте создадим новый проект. Для
этого запускаем PyCharm и в открывшимся окне выбираем New Project.

<i>Рис. 2</i>

Рис. 2

Указываем
имя проекта и нажимаем Create.

Рис. 3

Рис. 3

Напишем первый тест, чтобы проверить работоспособность драйвера.

<i>Рис. 4. Пример кода в файле main.py</i>

Рис. 4. Пример кода в файле main.py

В качестве примера ресурса для тестирования возьмем
популярный сайт для практики автоматизированного тестирования: https://www.saucedemo.com.

Кейс:

  • Зайти на страницу.
  • Найти элемент по id.
  • Вывести в консоль сообщение с результатом поиска.
main.py
        from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_experimental_option("excludeSwitches", ["enable-logging"])
driver = webdriver.Chrome(options=options, executable_path=r'C:/Users/.../.../chromedriver.exe')
driver.get("https://www.saucedemo.com/")
input_username = driver.find_element_by_id("user-name")
if input_username is None:
   print("Элемент не найден")
else:
   print("Элемент найден")

    

После
ввода кода необходимо установить библиотеку Selenium в наш проект.

Для
этого нажмите на подсвеченный текст в редакторе, нажмите Alt + Enter и далее
выберите Install package selenium. Это нужно делать для
каждого неустановленного пакета.

<i>Рис. 5. Пример установки пакета в проект</i>

Рис. 5. Пример установки пакета в проект

Запустить сценарий можно во встроенном эмуляторе терминала IDE или в любом другом:

        python main.py
    

<i>Рис. 6. <span>Пример
запуска скрипта из IDE</span></i>

Рис. 6. Пример
запуска скрипта из IDE

Если все установлено правильно, должен запуститься браузер,
который откроет страницу. Результатом запуска нашего сценария на Python, будет
сообщение: “Элемент найден”.

Рис. 7. Результат выполнения скрипта.

Рис. 7. Результат выполнения скрипта.

3. Поиск элементов

В нашем скрипте присутствует следующая строка:

        input_username = driver.find_element_by_id("user-name")
    

Метод find_element_by_id позволяет процессу найти элемент в разметке HTML по наименованию атрибута id. В реализации драйвера есть несколько способов поиска элементов на странице: по name, xpath, css, id. Поиск по css и xpath являются более универсальным, но он сложнее для начинающих. Использование поиска по name и id намного удобнее, но в практической разработке используется редко. Далее я буду использовать только xpath.

Теперь
давайте напишем кейс аутентификации пользователя на странице входа:

  • Шаг 1: пользователь вводит корректный username и password.
  • Шаг 2: нажимает кнопку ввода.
  • Ожидаемый результат: пользователь попадает на главную страницу магазина. Проверка заголовка на соответствие “PRODUCTS”.
main.py
        import time

from selenium import webdriver
from selenium.webdriver.common.keys import Keys


def first_test():
    options = webdriver.ChromeOptions()
    options.add_experimental_option("excludeSwitches", ["enable-logging"])
    driver = webdriver.Chrome(options=options, executable_path=r'C:/Users/.../.../chromedriver.exe')
    driver.get("https://www.saucedemo.com/")

    # Поиск элементов и присваивание к переменным.
    input_username = driver.find_element_by_xpath("//*[@id="user-name"]")
    input_password = driver.find_element_by_xpath("//*[@id="password"]")
    login_button = driver.find_element_by_xpath("//*[@id="login-button"]")

    # Действия с формами
    input_username.send_keys("standard_user")
    input_password.send_keys("secret_sauce")
    login_button.send_keys(Keys.RETURN)

    # Поиск и проверка попадания на главную страницу
    title_text = driver.find_element_by_xpath("//*[@id="header_container"]/div[2]/span")
    if title_text.text == "PRODUCTS":
        print("Мы попали на главную страницу")
    else:
        print("Ошибка поиска элемента")

    time.sleep(5)


if __name__ == '__main__':
    first_test()

    

Разберем
пример пошагово:

  • Для работы с формой найдем и присвоим элементы переменным input_username, input_password и login_button с помощью xpath.
  • Далее вызовем для элемента метод send_keys с данными, которые хотим передать в текстовое поле. В нашем случае в username отправляем «standart_user», в password«secret_sauce». Проецируя поведение пользователя нажимаем Enter для ввода данных, используя метод send_keys для найденной кнопки с переданным аргументом Keys.RETURN. Этот аргумент позволяет работать с действиями клавиатуры в Selenium, аналогично нажатию на Enter на клавиатуре.
  • На главном экране нам необходимо найти и присвоить переменной элемент текста Products. Как я говорил раннее, не всегда есть возможность найти элемент по id – здесь как раз тот случай.
        title_text = driver.find_element_by_xpath("//*[@id="header_container"]/div[2]/span")
    
  • Путь xpath до элемента: //*[@id="header_container"]/div[2]/span.
  • Чтобы найти путь xpath, зайдите на https://www.saucedemo.com и нажмите F12, чтобы открыть инструменты разработчика. Затем выберите стрелку-указатель и кликните по элементу до которого хотите найти путь. В нашем случае до Products.

<i>Рис 8. Поиск xpath элемента в инструментах разработчика</i>

Рис 8. Поиск xpath элемента в инструментах разработчика
  • Откроется код элемента в дереве HTML, далее нужно открыть контекстное меню выделенной строки и скопировать xpath.

<i>Рис 9. Копирование пути xpath</i>

Рис 9. Копирование пути xpath

Если кратко рассматривать путь, то //* обозначает, что будут найдены все элементы на странице, а [@id="header_container"] обозначает условие поиска (будут найдены все элементы на странице с тэгом id = "header_container").И далее /div[2]/span – спускаемся на второй дочерний элемент div и далее на дочерний элемент span. Сравните полученный xpath с деревом элемента в инструментах разработчика – сразу станет понятно что к чему.

  • Тут мы просто сравниваем текст найденного элемента с ожидаемым значением и выводим в консоль сообщение.
main.py
        if title_text.text == "PRODUCTS":
    print("Мы попали на главную страницу")
else:
    print("Ошибка поиска элемента")

    

При выполнении скрипта получили следующий результат:

Рис 10. Результат выполнения скрипта

Рис 10. Результат выполнения скрипта

4. Первый тест с поиском и переходом по странице

Кейс:

  • Введем логин и пароль пользователя и зайдем на главную страницу.
  • Найдем позицию с названием «Sauce Labs Fleece Jacket».
  • Перейдем на страницу товара и нажмем кнопку добавления в корзину.
  • Перейдем в корзину и проверим что там присутствует 1 позиция с названием «Sauce Labs Fleece Jacket».
main.py
        import time

from selenium import webdriver
from selenium.webdriver.common.keys import Keys


def first_test():
    options = webdriver.ChromeOptions()
    options.add_experimental_option("excludeSwitches", ["enable-logging"])
    driver = webdriver.Chrome(options=options, executable_path=r'C:/Users/…/…/chromedriver.exe')
    driver.get("https://www.saucedemo.com/")

    # Поиск элементов и присваивание к переменным.
    input_username = driver.find_element_by_xpath("//*[@id="user-name"]")
    input_password = driver.find_element_by_xpath("//*[@id="password"]")
    login_button = driver.find_element_by_xpath("//*[@id="login-button"]")

    # Действия с формами
    input_username.send_keys("standard_user")
    input_password.send_keys("secret_sauce")
    login_button.send_keys(Keys.RETURN)

    # Поиск ссылки элемента позиции магазина и клик по ссылке
    item_name = driver.find_element_by_xpath("//*[@id="item_5_title_link"]/div")
    item_name.click()

    # Поиск кнопки добавления товара и клик по этой кнопке
    item_add_button = driver.find_element_by_xpath("//*[@id="add-to-cart-sauce-labs-fleece-jacket"]")
    item_add_button.click()

    # Поиск кнопки коризины и клик по этой кнопке
    shopping_cart = driver.find_element_by_xpath("//*[@id="shopping_cart_container"]/a")
    shopping_cart.click()

    # Еще один поиск ссылки элемента позиции магазина
    item_name = driver.find_element_by_xpath("//*[@id="item_5_title_link"]/div")
    if item_name.text == "Sauce Labs Fleece Jacket":
        print("Товар пристутствует в корзине")
    else:
        print("Товар отсутствует")

    time.sleep(5)


if __name__ == '__main__':
    first_test()

    

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

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

Ожидания в selenium: что нужно знать?

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

Selenium driver поддерживает два вида ожиданий: явное (explicit) и неявное (implicity). Для явных ожиданий есть специальные методы, которые помогут рационально использовать время выполнения теста: например, можно установить минимальное время ожидания и возвращать элемент, если он прогрузился раньше предполагаемого времени.

Пример явного ожидания:

        element = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable(
        (By.XPATH, '//*[@id="page_wrapper"]/footer/ul/li[2]/a')
    )
)

    

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

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

Пример неявного ожидания:

        driver.implicitly_wait(10)
    

Ожидать действия можно и с помощью time.sleep(5). У нас в
примерах есть использование этого метода, но оно считается плохой практикой и обычно применяется только для дебага.

5. Рефакторинг теста, добавление ожиданий

Чтобы
pytest понял, что перед ним именно тестовая, а не обычная функция, сама тестовая функция
должна начинаться с test_.

Обновим наш тест, добавим необходимые ожидания для
стабильности тестовых функций.

Также я вынес отдельную функцию под ожидания, куда мы просто
передаем xpath и driver в виде аргументов.

main.py
        from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
import time

from selenium import webdriver
from selenium.webdriver.common.keys import Keys


# Функция ожидания элементов
def wait_of_element_located(xpath, driver):
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located(
            (By.XPATH, xpath)
        )
    )
    return element


def test_add_jacket_to_the_shopcart():
    options = webdriver.ChromeOptions()
    options.add_experimental_option("excludeSwitches", ["enable-logging"])
driver = webdriver.Chrome(options=options, executable_path=r'C:/Users/…/…/chromedriver.exe')
    driver.get("https://www.saucedemo.com/")

    # Поиск и ожидание элементов и присваивание к переменным.
    input_username = wait_of_element_located(xpath='//*[@id="user-name"]', driver=driver)
    input_password = wait_of_element_located(xpath='//*[@id="password"]', driver=driver)
    login_button = wait_of_element_located(xpath='//*[@id="login-button"]', driver=driver)

    # Действия с формами
    input_username.send_keys("standard_user")
    input_password.send_keys("secret_sauce")
    login_button.send_keys(Keys.RETURN)

    # Поиск и ождиание прогрузки ссылки элемента товара магазина и клик по ссылке
    item_name = wait_of_element_located(xpath='//*[@id="item_5_title_link"]/div', driver=driver)
    item_name.click()

    # Поиск и ожидание кнопки добавления товара и клик по этой кнопке
    item_add_button = wait_of_element_located(xpath='//*[@id="add-to-cart-sauce-labs-fleece-jacket"]', driver=driver)
    item_add_button.click()

    # Ждем пока товар добавится в корзину, появится span(кол-во позиций в корзине) и кликаем по корзине чтобы перейти
    wait_of_element_located(xpath='//*[@id="shopping_cart_container"]/a/span', driver=driver).click()

    # Еще один поиск ссылки элемента позиции магазина
    item_name = wait_of_element_located(xpath='//*[@id="item_5_title_link"]/div', driver=driver)
    if item_name.text == "Sauce Labs Fleece Jacket":
        print("Товар пристутствует в корзине")
    else:
        print("Товар отсутствует")

    time.sleep(5)


if __name__ == '__main__':
    test_add_jacket_to_the_shopcart()

    

Для запуска теста с помощью pytest в терминале введите
pytest main.py. После прохождения всех этапов должен отобразиться результат
прохождения.

6. Проверки, проверки, проверки

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

Добавим в тест проверки. Будем проверять, что название
куртки «Sauce Labs Fleece Jacket» и описание как в магазине.

main.py
        from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait

from selenium import webdriver
from selenium.webdriver.common.keys import Keys


# Функция ожидания элементов
def wait_of_element_located(xpath, driver):
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located(
            (By.XPATH, xpath)
        )
    )
    return element


def test_add_jacket_to_the_shopcart():
    options = webdriver.ChromeOptions()
    options.add_experimental_option("excludeSwitches", ["enable-logging"])
    driver = webdriver.Chrome(options=options, executable_path=r'C:/Users/…/…/chromedriver.exe')
    driver.get("https://www.saucedemo.com/")

    # Поиск и ожидание элементов и присваивание к переменным.
    input_username = wait_of_element_located(xpath='//*[@id="user-name"]', driver=driver)
    input_password = wait_of_element_located(xpath='//*[@id="password"]', driver=driver)
    login_button = wait_of_element_located(xpath='//*[@id="login-button"]', driver=driver)

    # Действия с формами
    input_username.send_keys("standard_user")
    input_password.send_keys("secret_sauce")
    login_button.send_keys(Keys.RETURN)

    # Поиск и ождиание прогрузки ссылки элемента товара магазина и клик по ссылке
    item_name = wait_of_element_located(xpath='//*[@id="item_5_title_link"]/div', driver=driver)
    item_name.click()

    # Поиск и ожидание кнопки добавления товара и клик по этой кнопке
    item_add_button = wait_of_element_located(xpath='//*[@id="add-to-cart-sauce-labs-fleece-jacket"]', driver=driver)
    item_add_button.click()

    # Ждем пока товар добавится в корзину, появится span(кол-во позиций в корзине) и кликаем по корзине чтобы перейти
    wait_of_element_located(xpath='//*[@id="shopping_cart_container"]/a/span', driver=driver).click()

    # Еще один поиск ссылки элемента позиции магазина
    item_name = wait_of_element_located(xpath='//*[@id="item_5_title_link"]/div', driver=driver)

    item_description = wait_of_element_located(
        xpath='//*[@id="cart_contents_container"]/div/div[1]/div[3]/div[2]/div[1]',
        driver=driver
    )

    assert item_name.text == "Sauce Labs Fleece Jacket"
    assert item_description.text == "It's not every day that you come across a midweight quarter-zip fleece jacket capable of handling everything from a relaxing day outdoors to a busy day at the office."

    driver.close()


if __name__ == '__main__':
    test_add_jacket_to_the_shopcart()

    

Теперь при расхождении результата и ожидаемого
условия будет возвращена ошибка прохождения. Укажем название куртки «Sauce Labs Fleece Jacket1». Результат выполнения скрипта будет следующим:

Рис 11. Результат выполнения теста.

Рис 11. Результат выполнения теста.

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

main.py
        import pytest
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait

from selenium import webdriver
from selenium.webdriver.common.keys import Keys


# Функция ожидания элементов
def wait_of_element_located(xpath, driver_init):
    element = WebDriverWait(driver_init, 10).until(
        EC.presence_of_element_located(
            (By.XPATH, xpath)
        )
    )
    return element


# Вынесем инициализцию драйвера в отдельную фикстуру pytest
@pytest.fixture
def driver_init():
    options = webdriver.ChromeOptions()
    options.add_experimental_option("excludeSwitches", ["enable-logging"])
    driver = webdriver.Chrome(options=options, executable_path=r'C:/Users/…/…/chromedriver.exe')
    driver.get("https://www.saucedemo.com/")
    yield driver
    driver.close()


# Вынесем аутентификацию юзера в отдельную функцию
def auth_user(user_name, password, driver_init):
    # Поиск и ожидание элементов и присваивание к переменным.
    input_username = wait_of_element_located(xpath='//*[@id="user-name"]', driver_init=driver_init)
    input_password = wait_of_element_located(xpath='//*[@id="password"]', driver_init=driver_init)
    login_button = wait_of_element_located(xpath='//*[@id="login-button"]', driver_init=driver_init)

    # Действия с формами
    input_username.send_keys(user_name)
    input_password.send_keys(password)
    login_button.send_keys(Keys.RETURN)



def add_item_to_cart(xpath_item, driver_init):
    # Поиск и ождиание прогрузки ссылки элемента товара магазина и клик по ссылке
    item_name = wait_of_element_located(
        xpath=xpath_item,
        driver_init=driver_init)
    item_name.click()

    # Поиск и ожидание кнопки добавления товара и клик по этой кнопке
    item_add_button = wait_of_element_located(
        xpath='//*[@id="add-to-cart-sauce-labs-fleece-jacket"]',
        driver_init=driver_init)
    item_add_button.click()

    # Ждем пока товар добавится в корзину, появится span(кол-во позиций в корзине)
    # Возвращаем True или False в зависимости добавлися товар или нет
    shop_cart_with_item = wait_of_element_located(
        xpath='//*[@id="shopping_cart_container"]/a/span',
        driver_init=driver_init)
    return shop_cart_with_item


def test_add_jacket_to_the_shopcart(driver_init):
    # Аутентификация пользователя
    auth_user("standard_user", "secret_sauce", driver_init=driver_init)

    # Добавление товара в корзину и если товар добавлен переход в корзину
    add_item_to_cart(xpath_item='//*[@id="item_5_title_link"]/div',
                     driver_init=driver_init).click()
    # Поиск корзины и клик
    wait_of_element_located(xpath='//*[@id="shopping_cart_container"]/a',
                            driver_init=driver_init).click()

    # Поиск ссылки элемента позиции магазина
    item_name = wait_of_element_located(xpath='//*[@id="item_5_title_link"]/div',
                                        driver_init=driver_init)

    # Поиск описания товара
    item_description = wait_of_element_located(xpath='//*[@id="cart_contents_container"]/div/div[1]/div[3]/div[2]/div[1]',
                                               driver_init=driver_init)

    assert item_name.text == "Sauce Labs Fleece Jacket"
    assert item_description.text == "It's not every day that you come across a midweight quarter-zip fleece jacket" 
                                    " capable of handling everything from a relaxing day outdoors to a busy day at " 
                                    "the office."


if __name__ == '__main__':
    test_add_jacket_to_the_shopcart(driver_init=driver_init)

    

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

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

При желании можно и дальше проводить рефакторинг кода.

Рекомендации по архитектуре

  • Очевидно, что в одном файле хранить все вспомогательные функции и тесты неудобно. После добавления еще нескольких тестов даже с распределенной логикой скрипт будет похож на полотно с трудночитаемым кодом. К тому же если вы разрабатываете тесты с коллегами, без конфликтов в коде не обойтись. Для начала нужно разделить проект на модули: в одном будут находиться файлы с тестами, в другом частичная логика, в третьем – ресурсы, в четвертом – утилиты и т.д.
  • Далее следует переходить на разработку автотестов с использованием объектно-ориентированного программирования. Это сэкономит массу времени и поможет в написании сложного и лаконичного кода.
  • Стоит также обратить внимание на паттерны проектирования, особенно на PageObject и PageFactoroy. В эффективном тестировании UI они играют большую роль.
  • Все тестовые данные лучше хранить в неизменяемых классах, константах или в отдельных файлах (json, csv).

Заключение

На этом создание первого автотеста закончено. Некоторые
моменты были рассмотрены поверхностно, что дает возможность пытливым умам
поглотить информацию из других источников. Удачи!

Автор: Энди Найт (Andy Knight)
Оригинал статьи
Перевод: Ольга Алифанова

Часть 1

Теперь, когда мы знаем, зачем нам Web UI-тестирование и какими должны быть наши цели, настроим проект тест-автоматизации на Python, используя pytest!

Почему Python?

Python – один из самых популярных из доступных языков программирования. На нем пишут бэкэнды сайтов, проводят анализ данных, создают скрипты для администрирования системы… Его синтаксис чист, читабелен и элегантен, и отлично подходит как для экспертов, так и для начинающих. К тому же все мыслимое и немыслимое всего лишь в одном импорте от вас. Логично, что Python также отлично подходит для тест-автоматизации. Его лаконичность позволяет больше фокусироваться на тесте, и меньше – на коде. Тестировщики без особых навыков программирования, как правило, изучают Python быстрее, нежели другие языки вроде Java или C#. Python прекрасен для быстрого погружения в автоматизацию!

Что такое pytest?

В сердце любого проекта функциональной тест-автоматизации лежит «корневой» тест-фреймворк. Фреймворк имеет дело со структурой тест-кейсов, выполнением тестов, и отчетности о пройденных и упавших проверках. Это основа, к которой добавляются дополнительные пакеты и код (вроде Selenium WebDriver).

pytest – один из лучших тест-фреймворков Python. Он прост, масштабируем, и «Пайтоничен». Тест-кейсы пишутся как функции, а не классы. Падения тест-утверждений выводятся в отчете с реальными значения ми. При помощи плагинов можно добавить покрытие кода, красивые отчеты, и параллельный запуск. pytest может интегрироваться с другими фреймворками вроде Django и Flask. Согласно опросу разработчиков Python-2018, pytest – самый популярный тест-фреймворк для Python.

Приступим

Давайте создадим наш первый тест-проект на Python! Если вы еще этого не сделали, пожалуйста, скачайте и установите Python 3. Затем создайте новую директорию для проекта:

  1. $ mkdir python-webui-testing
  2. $ cd python-webui-testing

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

  1. $ pip install pipenv

Затем установите pytest для вашего нового проекта:

  1. $ pipenv install pytest —dev

Pipenv добавит в ваш проект два новых файла: Pipfile и Pipfile.lock. Pipfile определяет требования проекта, а Pipfile.lock «блокирует» явные версии, которыми проект будет пользоваться. Опция “–dev” в команде означает, что пакет pytest будет использоваться для разработки, а не для деплоя.

Первый тест

По традиции большинство проектов размещают все тесты в директории tests/. Давайте следовать традиции:

  1. $ mkdir tests
  2. $ cd tests

Создайте модуль Python с названием test_math.py для нашего первого теста, и добавьте следующий код:

  1. def test_addition():
  2. assert 1 + 1 == 2

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

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

Давайте запустим наш новый тест. Смените директорию на корневую директорию проекта, и вызовите модуль pytest:

  1. $ cd ..
  2. $ pipenv run python -m pytest
  3. ============================= test session starts ==============================
  4. platform darwin — Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  5. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  6. collected 1 item
  7. tests/test_math.py . [100%]
  8. =========================== 1 passed in 0.02 seconds ===========================

Наш первый тест пройден!

Как pytest обнаружил наш тест? По имени: pytest будет искать тест-функции с именем test_* в модулях по имени test_*.py. Интересно, что pytest не требует файла __init__.py в тест-директориях.

Упавшие тесты

Что будет, если тест упадет? Давайте добавим другой тест с багом, чтобы проверить это:

  1. def test_subtraction():
  2. diff = 1 — 1
  3. assert diff == 1

Теперь при запуске Pytest мы увидим вот что:

  1. $ pipenv run python -m pytest
  2. ============================= test session starts ==============================
  3. platform darwin — Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 2 items
  6. tests/test_math.py .F [100%]
  7. =================================== FAILURES ===================================
  8. _______________________________ test_subtraction _______________________________
  9. def test_subtraction():
  10. diff = 1 — 1
  11. > assert diff == 1
  12. E assert 0 == 1
  13. tests/test_math.py:13: AssertionError
  14. ====================== 1 failed, 1 passed in 0.08 seconds ======================

Тест test_subtraction упал с «F» вместо «.». Более того, pytest выводит сообщения трассировки, показывающие упавшее утверждение вместе с модулем и номером строки. Отметим, что реальные значения каждого выражения в утверждении тоже отображаются: diff оценивается как 0, что явно не равно 1. Круто! Эта самодиагностика утверждений очень помогает разбираться в падениях тестов.

Исправим баг:

  1. def test_subtraction():
  2. diff = 1 — 1
  3. assert diff == 0

И перезапустим эти тесты:

  1. $ pipenv run python -m pytest
  2. ============================= test session starts ==============================
  3. platform darwin — Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 2 items
  6. tests/test_math.py .. [100%]
  7. =========================== 2 passed in 0.02 seconds ===========================

Мы снова на верном пути.

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

Что, если нам нужно запустить тест-процедуру с разными сочетаниями ввода? У pytest для этого есть декоратор! Составим тест для умножения с параметризованным вводом:

  1. import pytest
  2. @pytest.mark.parametrize(
  3. «a,b,expected»,
  4. [(0, 5, 0), (1, 5, 5), (2, 5, 10), (-3, 5, -15), (-4, -5, 20)])
  5. def test_multiplication(a, b, expected):
  6. assert a * b == expected

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

  1. $ pipenv run python -m pytest
  2. ============================= test session starts ==============================
  3. platform darwin — Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 7 items
  6. tests/test_math.py ……. [100%]
  7. =========================== 7 passed in 0.03 seconds ===========================

Супер! Параметры – отличный способ реализовать тестирование, управляемое через данные.

Sweet! Parameters are a great way to do data-driven testing.

Верификация исключений

Pytest относится к необрабатываемым исключениям, как к падениям теста. На самом деле оператор контроля просто выдает исключение для регистрации падения. Что, если нам нужно убедиться, что выдается правильное исключение? Используйте pytest.raises с нужным типом исключения – например, так:

  1. def test_divide_by_zero():
  2. with pytest.raises(ZeroDivisionError):
  3. 1 / 0

Rerun the tests to make sure all is well:

  1. $ pipenv run python -m pytest
  2. ============================= test session starts ==============================
  3. platform darwin — Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 8 items
  6. tests/test_math.py …….. [100%]
  7. =========================== 8 passed in 0.04 seconds ===========================

Отлично. Математика все еще работает!

Больше информации

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

Software testing is the process in which a developer ensures that the actual output of the software matches with the desired output by providing some test inputs to the software. Software testing is an important step because if performed properly, it can help the developer to find bugs in the software in very less amount of time. Software testing can be divided into two classes, Manual testing and Automated testing. Automated testing is the execution of your tests using a script instead of a human. In this article, we’ll discuss some of the methods of automated software testing with Python. Let’s write a simple application over which we will perform all the tests. 

Python3

class Square:

    def __init__(self, side):

        """ creates a square having the given side

        """

        self.side = side

    def area(self):

        """ returns area of the square

        """

        return self.side**2

    def perimeter(self):

        """ returns perimeter of the square

        """

        return 4 * self.side

    def __repr__(self):

        """ declares how a Square object should be printed

        """

        s = 'Square with side = ' + str(self.side) + 'n' +

        'Area = ' + str(self.area()) + 'n' +

        'Perimeter = ' + str(self.perimeter())

        return s

if __name__ == '__main__':

    side = int(input('enter the side length to create a Square: '))

    square = Square(side)

    print(square)

Note: For more information about the function __repr__(), refer this article. Now that we have our software ready, let’s have a look at the directory structure of our project folder and after that, we’ll start testing our software.

---Software_Testing
   |--- __init__.py (to initialize the directory as python package)
   |--- app.py (our software)
   |--- tests (folder to keep all test files)
           |--- __init__.py

The ‘unittest’ module

One of the major problems with manual testing is that it requires time and effort. In manual testing, we test the application over some input, if it fails, either we note it down or we debug the application for that particular test input, and then we repeat the process. With unittest, all the test inputs can be provided at once and then you can test your application. In the end, you get a detailed report with all the failed test cases clearly specified, if any. The unittest module has both a built-in testing framework and a test runner. A testing framework is a set of rules which must be followed while writing test cases, while a test runner is a tool which executes these tests with a bunch of settings, and collects the results. Installation: unittest is available at PyPI and can be installed with the following command –

pip install unittest

Use: We write the tests in a Python module (.py). To run our tests, we simply execute the test module using any IDE or terminal. Now, let’s write some tests for our small software discussed above using the unittest module.

  1. Create a file named tests.py in the folder named “tests”.
  2. In tests.py import unittest.
  3. Create a class named TestClass which inherits from the class unittest.TestCase. Rule 1: All the tests are written as the methods of a class, which must inherit from the class unittest.TestCase.
  4. Create a test method as shown below. Rule 2: Name of each and every test method should start with “test” otherwise it’ll be skipped by the test runner. 

Python3

def test_area(self):

    sq = Square(2)   

    self.assertEqual(sq.area(), 4,

        f'Area is shown {sq.area()} for side = {sq.side} units')

  1. Rule 3: We use special assertEqual() statements instead of the built-in assert statements available in Python. The first argument of assertEqual() is the actual output, the second argument is the desired output and the third argument is the error message which would be displayed in case the two values differ from each other (test fails).
  2. To run the tests we just defined, we need to call the method unittest.main(), add the following lines in the “tests.py” module. 

Python3

if __name__ == '__main__':

    unittest.main()

  1. Because of these lines, as soon as you run execute the script “test.py”, the function unittest.main() would be called and all the tests will be executed.

Finally the “tests.py” module should resemble the code given below. 

Python3

import unittest

from .. import app

class TestSum(unittest.TestCase):

    def test_area(self):

        sq = app.Square(2)

        self.assertEqual(sq.area(), 4,

            f'Area is shown {sq.area()} rather than 9')

if __name__ == '__main__':

    unittest.main()

Having written our test cases let us now test our application for any bugs. To test your application you simply need to execute the test file “tests.py” using the command prompt or any IDE of your choice. The output should be something like this.

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

In the first line, a .(dot) represents a successful test while an ‘F’ would represent a failed test case. The OK message, in the end, tells us that all the tests were passed successfully. Let’s add a few more tests in “tests.py” and retest our application. 

Python3

import unittest

from .. import app

class TestSum(unittest.TestCase):

    def test_area(self):

        sq = app.Square(2)

        self.assertEqual(sq.area(), 4,

            f'Area is shown {sq.area()} rather than 9')

    def test_area_negative(self):

        sq = app.Square(-3)

        self.assertEqual(sq.area(), -1,

            f'Area is shown {sq.area()} rather than -1')

    def test_perimeter(self):

        sq = app.Square(5)

        self.assertEqual(sq.perimeter(), 20,

            f'Perimeter is {sq.perimeter()} rather than 20')

    def test_perimeter_negative(self):

        sq = app.Square(-6)

        self.assertEqual(sq.perimeter(), -1,

            f'Perimeter is {sq.perimeter()} rather than -1')

if __name__ == '__main__':

    unittest.main()

.F.F
======================================================================
FAIL: test_area_negative (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests_unittest.py", line 11, in test_area_negative
    self.assertEqual(sq.area(), -1, f'Area is shown {sq.area()} rather than -1 for negative side length')
AssertionError: 9 != -1 : Area is shown 9 rather than -1 for negative side length

======================================================================
FAIL: test_perimeter_negative (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests_unittest.py", line 19, in test_perimeter_negative
    self.assertEqual(sq.perimeter(), -1, f'Perimeter is {sq.perimeter()} rather than -1 for negative side length')
AssertionError: -24 != -1 : Perimeter is -24 rather than -1 for negative side length

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=2)

A few things to note in the above test report are –

  • The first line represents that test 1 and test 3 executed successfully while test 2 and test 4 failed
  • Each failed test case is described in the report, the first line of the description contains the name of the failed test case and the last line contains the error message we defined for that test case.
  • At the end of the report you can see the number of failed tests, if no test fails the report will end with OK

Note: For further knowledge you can read the complete documentation of unittest.

The “nose2” module

The purpose of nose2 is to extend unittest to make testing easier. nose2 is compatible with tests written using the unittest testing framework and can be used as a replacement of the unittest test runner. Installation: nose2 can be installed from PyPI using the command,

pip install nose2

Use: nose2 does not have any testing framework and is merely a test runner which is compatible with the unittest testing framework. Therefore we’ll the run same tests we wrote above (for unittest) using nose2. To run the tests we use the following command in the project source directory (“Software_Testing” in our case),

nose2

In nose2 terminology all the python modules (.py) with name starting from “test” (i.e. test_file.py, test_1.py) are considered as test files. On execution, nose2 will look for all test files in all the sub-directories which lie under one or more of the following categories,

  • which are python packages (contain “__init__.py”).
  • whose name starts with “test” after being lowercased, i.e. TestFiles, tests.
  • which are named either “src” or “lib”.

nose2 first loads all the test files present in the project and then the tests are executed. Thus, with nose2 we get the freedom to split our tests among various test files in different folders and execute them at once, which is very useful when dealing with large number of tests. Let’s now learn about different customisation options provided by nose2 which can help us during the testing process.

  1. Changing the search directory – If we want to change the directory in which nose2 searches for test files, we can do that using the command line arguments -s or –start-dir as,
nose2 -s DIR_ADD DIR_NAME
  1. here, DIR_NAME is the directory in which we want to search for the test files and, DIR_ADD is the address of the parent directory of DIR_NAME relative to the project source directory (i.e. use “./” if test directory is in the project source directory itself). This is extremely useful when you want to test only one feature of your application at a time.
  2. Running specific test cases – Using nose2 we can also run a specific test at a time by using the command line arguments -s and –start-dir as,
nose2 -s DIR_ADD DIR_NAME.TEST_FILE.TEST_CLASS.TEST_NAME
  • TEST_NAME: name of the test method.
  • TEST_CLASS: class in which the test method is defined.
  • TEST_FILE: name of the test file in which the test case is defined i.e. test.py.
  • DIR_NAME: directory in which the test file exists.
  • DIR_ADD: address of the parent directory of DIR_NAME relative to the project source.
  1. Running tests in a single module – nose2 can also be used like unittest by calling the function nose2.main() just like we called unittest.main() in previous examples.

The “pytest” module

pytest is the most popular testing framework for python. Using pytest you can test anything from basic python scripts to databases, APIs and UIs. Though pytest is mainly used for API testing, in this article we’ll cover only the basics of pytest. Installation: You can install pytest from PyPI using the command,

pip install pytest

Use: The pytest test runner is called using the following command in project source,

py.test

Unlike nose2, pytest looks for test files in all the locations inside the project directory. Any file with name starting with “test_” or ending with “_test” is considered a test file in the pytest terminology. Let’s create a file “test_file1.py” in the folder “tests” as our test file. Creating test methods: pytest supports the test methods written in the unittest framework, but the pytest framework provides easier syntax to write tests. See the code below to understand the test method syntax of the pytest framework. 

Python3

from .. import app

def test_file1_area():

    sq = app.Square(2)

    assert sq.area() == 4,

        f"area for side {sq.side} units is {sq.area()}"

def test_file1_perimeter():

    sq = app.Square(-1)

    assert sq.perimeter() == -1,

        f'perimeter is shown {sq.perimeter()} rather than -1'

Note: similar to unittest, pytest requires all test names to start with “test”. Unlike unittest, pytest uses the default python assert statements which make it further easier to use. Note that, now the “tests” folder contains two files namely, “tests.py” (written in unittest framework) and “test_file1.py” (written in pytest framework). Now let’s run the pytest test runner.

py.test

You’ll get a similar report as obtained by using unittest.

============================= test session starts ==============================
platform linux -- Python 3.6.7, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /home/manthan/articles/Software_testing_in_Python
collected 6 items                                                              

tests/test_file1.py .F                                                   [ 33%]
tests/test_file2.py .F.F                                                 [100%]

=================================== FAILURES ===================================

The percentages on the right side of the report show the percentage of tests that have been completed at that moment, i.e. 2 out of the 6 test cases were completed at the end of the “test_file1.py”. Here are a few more basic customisations that come with pytest.

  1. Running specific test files: To run only a specific test file, use the command,
py.test <filename>
  1. Substring matching: Suppose we want to test only the area() method of our Square class, we can do this using substring matching as follows,
py.test -k "area"
  1. With this command pytest will execute only those tests which have the string “area” in their names, i.e. “test_file1_area()”, “test_area()” etc.
  2. Marking: As a substitute to substring matching, marking is another method using which we can run a specific set of tests. In this method we put a mark on the tests we want to run. Observe the code example given below, 

Python3

@pytest.mark.area    

def test_file1_area():

    sq = app.Square(2)

    assert sq.area() == 4,

        f"area for side {sq.side} units is {sq.area()}"

  1. In the above code example test_file1_area() is marked with tag “area”. All the test methods which have been marked with some tag can be executed by using the command,
py.test -m <tag_name>
  1. Parallel Processing: If you have a large number of tests then pytest can be customised to run these test methods in parallel. For that you need to install pytest-xdist which can be installed using the command,
pip install pytest-xdist
  1. Now you can use the following command to execute your tests faster using multiprocessing,
py.test -n 4
  1. With this command pytest assigns 4 workers to perform the tests in parallel, you can change this number as per your needs. If your tests are thread-safe, you can also use multithreading to speed up the testing process. For that you need to install pytest-parallel (using pip). To run your tests in multithreading use the command,
pytest --workers 4

Software testing is the process in which a developer ensures that the actual output of the software matches with the desired output by providing some test inputs to the software. Software testing is an important step because if performed properly, it can help the developer to find bugs in the software in very less amount of time. Software testing can be divided into two classes, Manual testing and Automated testing. Automated testing is the execution of your tests using a script instead of a human. In this article, we’ll discuss some of the methods of automated software testing with Python. Let’s write a simple application over which we will perform all the tests. 

Python3

class Square:

    def __init__(self, side):

        """ creates a square having the given side

        """

        self.side = side

    def area(self):

        """ returns area of the square

        """

        return self.side**2

    def perimeter(self):

        """ returns perimeter of the square

        """

        return 4 * self.side

    def __repr__(self):

        """ declares how a Square object should be printed

        """

        s = 'Square with side = ' + str(self.side) + 'n' +

        'Area = ' + str(self.area()) + 'n' +

        'Perimeter = ' + str(self.perimeter())

        return s

if __name__ == '__main__':

    side = int(input('enter the side length to create a Square: '))

    square = Square(side)

    print(square)

Note: For more information about the function __repr__(), refer this article. Now that we have our software ready, let’s have a look at the directory structure of our project folder and after that, we’ll start testing our software.

---Software_Testing
   |--- __init__.py (to initialize the directory as python package)
   |--- app.py (our software)
   |--- tests (folder to keep all test files)
           |--- __init__.py

The ‘unittest’ module

One of the major problems with manual testing is that it requires time and effort. In manual testing, we test the application over some input, if it fails, either we note it down or we debug the application for that particular test input, and then we repeat the process. With unittest, all the test inputs can be provided at once and then you can test your application. In the end, you get a detailed report with all the failed test cases clearly specified, if any. The unittest module has both a built-in testing framework and a test runner. A testing framework is a set of rules which must be followed while writing test cases, while a test runner is a tool which executes these tests with a bunch of settings, and collects the results. Installation: unittest is available at PyPI and can be installed with the following command –

pip install unittest

Use: We write the tests in a Python module (.py). To run our tests, we simply execute the test module using any IDE or terminal. Now, let’s write some tests for our small software discussed above using the unittest module.

  1. Create a file named tests.py in the folder named “tests”.
  2. In tests.py import unittest.
  3. Create a class named TestClass which inherits from the class unittest.TestCase. Rule 1: All the tests are written as the methods of a class, which must inherit from the class unittest.TestCase.
  4. Create a test method as shown below. Rule 2: Name of each and every test method should start with “test” otherwise it’ll be skipped by the test runner. 

Python3

def test_area(self):

    sq = Square(2)   

    self.assertEqual(sq.area(), 4,

        f'Area is shown {sq.area()} for side = {sq.side} units')

  1. Rule 3: We use special assertEqual() statements instead of the built-in assert statements available in Python. The first argument of assertEqual() is the actual output, the second argument is the desired output and the third argument is the error message which would be displayed in case the two values differ from each other (test fails).
  2. To run the tests we just defined, we need to call the method unittest.main(), add the following lines in the “tests.py” module. 

Python3

if __name__ == '__main__':

    unittest.main()

  1. Because of these lines, as soon as you run execute the script “test.py”, the function unittest.main() would be called and all the tests will be executed.

Finally the “tests.py” module should resemble the code given below. 

Python3

import unittest

from .. import app

class TestSum(unittest.TestCase):

    def test_area(self):

        sq = app.Square(2)

        self.assertEqual(sq.area(), 4,

            f'Area is shown {sq.area()} rather than 9')

if __name__ == '__main__':

    unittest.main()

Having written our test cases let us now test our application for any bugs. To test your application you simply need to execute the test file “tests.py” using the command prompt or any IDE of your choice. The output should be something like this.

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

In the first line, a .(dot) represents a successful test while an ‘F’ would represent a failed test case. The OK message, in the end, tells us that all the tests were passed successfully. Let’s add a few more tests in “tests.py” and retest our application. 

Python3

import unittest

from .. import app

class TestSum(unittest.TestCase):

    def test_area(self):

        sq = app.Square(2)

        self.assertEqual(sq.area(), 4,

            f'Area is shown {sq.area()} rather than 9')

    def test_area_negative(self):

        sq = app.Square(-3)

        self.assertEqual(sq.area(), -1,

            f'Area is shown {sq.area()} rather than -1')

    def test_perimeter(self):

        sq = app.Square(5)

        self.assertEqual(sq.perimeter(), 20,

            f'Perimeter is {sq.perimeter()} rather than 20')

    def test_perimeter_negative(self):

        sq = app.Square(-6)

        self.assertEqual(sq.perimeter(), -1,

            f'Perimeter is {sq.perimeter()} rather than -1')

if __name__ == '__main__':

    unittest.main()

.F.F
======================================================================
FAIL: test_area_negative (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests_unittest.py", line 11, in test_area_negative
    self.assertEqual(sq.area(), -1, f'Area is shown {sq.area()} rather than -1 for negative side length')
AssertionError: 9 != -1 : Area is shown 9 rather than -1 for negative side length

======================================================================
FAIL: test_perimeter_negative (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests_unittest.py", line 19, in test_perimeter_negative
    self.assertEqual(sq.perimeter(), -1, f'Perimeter is {sq.perimeter()} rather than -1 for negative side length')
AssertionError: -24 != -1 : Perimeter is -24 rather than -1 for negative side length

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=2)

A few things to note in the above test report are –

  • The first line represents that test 1 and test 3 executed successfully while test 2 and test 4 failed
  • Each failed test case is described in the report, the first line of the description contains the name of the failed test case and the last line contains the error message we defined for that test case.
  • At the end of the report you can see the number of failed tests, if no test fails the report will end with OK

Note: For further knowledge you can read the complete documentation of unittest.

The “nose2” module

The purpose of nose2 is to extend unittest to make testing easier. nose2 is compatible with tests written using the unittest testing framework and can be used as a replacement of the unittest test runner. Installation: nose2 can be installed from PyPI using the command,

pip install nose2

Use: nose2 does not have any testing framework and is merely a test runner which is compatible with the unittest testing framework. Therefore we’ll the run same tests we wrote above (for unittest) using nose2. To run the tests we use the following command in the project source directory (“Software_Testing” in our case),

nose2

In nose2 terminology all the python modules (.py) with name starting from “test” (i.e. test_file.py, test_1.py) are considered as test files. On execution, nose2 will look for all test files in all the sub-directories which lie under one or more of the following categories,

  • which are python packages (contain “__init__.py”).
  • whose name starts with “test” after being lowercased, i.e. TestFiles, tests.
  • which are named either “src” or “lib”.

nose2 first loads all the test files present in the project and then the tests are executed. Thus, with nose2 we get the freedom to split our tests among various test files in different folders and execute them at once, which is very useful when dealing with large number of tests. Let’s now learn about different customisation options provided by nose2 which can help us during the testing process.

  1. Changing the search directory – If we want to change the directory in which nose2 searches for test files, we can do that using the command line arguments -s or –start-dir as,
nose2 -s DIR_ADD DIR_NAME
  1. here, DIR_NAME is the directory in which we want to search for the test files and, DIR_ADD is the address of the parent directory of DIR_NAME relative to the project source directory (i.e. use “./” if test directory is in the project source directory itself). This is extremely useful when you want to test only one feature of your application at a time.
  2. Running specific test cases – Using nose2 we can also run a specific test at a time by using the command line arguments -s and –start-dir as,
nose2 -s DIR_ADD DIR_NAME.TEST_FILE.TEST_CLASS.TEST_NAME
  • TEST_NAME: name of the test method.
  • TEST_CLASS: class in which the test method is defined.
  • TEST_FILE: name of the test file in which the test case is defined i.e. test.py.
  • DIR_NAME: directory in which the test file exists.
  • DIR_ADD: address of the parent directory of DIR_NAME relative to the project source.
  1. Running tests in a single module – nose2 can also be used like unittest by calling the function nose2.main() just like we called unittest.main() in previous examples.

The “pytest” module

pytest is the most popular testing framework for python. Using pytest you can test anything from basic python scripts to databases, APIs and UIs. Though pytest is mainly used for API testing, in this article we’ll cover only the basics of pytest. Installation: You can install pytest from PyPI using the command,

pip install pytest

Use: The pytest test runner is called using the following command in project source,

py.test

Unlike nose2, pytest looks for test files in all the locations inside the project directory. Any file with name starting with “test_” or ending with “_test” is considered a test file in the pytest terminology. Let’s create a file “test_file1.py” in the folder “tests” as our test file. Creating test methods: pytest supports the test methods written in the unittest framework, but the pytest framework provides easier syntax to write tests. See the code below to understand the test method syntax of the pytest framework. 

Python3

from .. import app

def test_file1_area():

    sq = app.Square(2)

    assert sq.area() == 4,

        f"area for side {sq.side} units is {sq.area()}"

def test_file1_perimeter():

    sq = app.Square(-1)

    assert sq.perimeter() == -1,

        f'perimeter is shown {sq.perimeter()} rather than -1'

Note: similar to unittest, pytest requires all test names to start with “test”. Unlike unittest, pytest uses the default python assert statements which make it further easier to use. Note that, now the “tests” folder contains two files namely, “tests.py” (written in unittest framework) and “test_file1.py” (written in pytest framework). Now let’s run the pytest test runner.

py.test

You’ll get a similar report as obtained by using unittest.

============================= test session starts ==============================
platform linux -- Python 3.6.7, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /home/manthan/articles/Software_testing_in_Python
collected 6 items                                                              

tests/test_file1.py .F                                                   [ 33%]
tests/test_file2.py .F.F                                                 [100%]

=================================== FAILURES ===================================

The percentages on the right side of the report show the percentage of tests that have been completed at that moment, i.e. 2 out of the 6 test cases were completed at the end of the “test_file1.py”. Here are a few more basic customisations that come with pytest.

  1. Running specific test files: To run only a specific test file, use the command,
py.test <filename>
  1. Substring matching: Suppose we want to test only the area() method of our Square class, we can do this using substring matching as follows,
py.test -k "area"
  1. With this command pytest will execute only those tests which have the string “area” in their names, i.e. “test_file1_area()”, “test_area()” etc.
  2. Marking: As a substitute to substring matching, marking is another method using which we can run a specific set of tests. In this method we put a mark on the tests we want to run. Observe the code example given below, 

Python3

@pytest.mark.area    

def test_file1_area():

    sq = app.Square(2)

    assert sq.area() == 4,

        f"area for side {sq.side} units is {sq.area()}"

  1. In the above code example test_file1_area() is marked with tag “area”. All the test methods which have been marked with some tag can be executed by using the command,
py.test -m <tag_name>
  1. Parallel Processing: If you have a large number of tests then pytest can be customised to run these test methods in parallel. For that you need to install pytest-xdist which can be installed using the command,
pip install pytest-xdist
  1. Now you can use the following command to execute your tests faster using multiprocessing,
py.test -n 4
  1. With this command pytest assigns 4 workers to perform the tests in parallel, you can change this number as per your needs. If your tests are thread-safe, you can also use multithreading to speed up the testing process. For that you need to install pytest-parallel (using pip). To run your tests in multithreading use the command,
pytest --workers 4

Андрей Смирнов

Андрей Смирнов


Python-разработчик, эксперт по автоматизации и преподаватель в Школе программистов МШП

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

Не секрет, что разработчики создают программы, которые рано или поздно становятся очень масштабными (если смотреть на количество строчек кода). А с этим приходит и большая ответственность за качество.

Сейчас расскажу, как unittest и pytest помогут найти ошибки в программах и исключить их в будущем.

Итак, тестирование

Каждый, кто писал первые программы (будь то классический «hello, world» или же калькулятор), всегда запускал тесты, чтобы проверить их работу.

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

Например, вам нужно ввести три числа (a, b, c) и найти корни квадратного уравнения. Для решения пишем код:

from math import sqrt

def square_eq_solver(a, b, c):
   result = []
   discriminant = b * b - 4 * a * c

   if discriminant == 0:
       result.append(-b / (2 * a))
   else:
       result.append((-b + sqrt(discriminant)) / (2 * a))
       result.append((-b - sqrt(discriminant)) / (2 * a))

   return result

def show_result(data):
   if len(data) > 0:
       for index, value in enumerate(data):
           print(f'Корень номер {index+1} равен {value:.02f}')
   else:
       print('Уравнение с заданными параметрами не имеет корней')

def main():
   a, b, c = map(int, input('Пожалуйста, введите три числа через пробел: ').split())
   result = square_eq_solver(a, b, c)
   show_result(result)

if __name__ == '__main__':
   main()

Сразу оговорюсь: любую задачу, какой бы она ни была краткой, я рассматриваю с позиции «когда-нибудь она вырастет и станет очень объёмной». Поэтому всегда стараюсь разделять программу на различные подпрограммы (ввод/обработка/вывод).

Возможно, вы уже заметили ошибку в коде. Однако иногда она может быть скрыта настолько глубоко, что её просто так не обнаружишь. И в таком случае единственный способ вывести ее на свет — протестировать код. Как это сделать?

— зная алгоритм нахождения корней уравнения, определяем наборы входных данных, которые будут переданы на вход программе;

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

— запускаем программу и передаем ей на вход исходные данные;

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

Например, для данной задачи можно подобрать следующие тесты:

  • 10x**2 = 0 — единственный корень x=0
  • 2x**2 + 5x — 3 = 0 — у такого уравнения два корня (x1 = 0.5, x2=-3)
  • 10x**2+2 = 0 — у этого уравнения корней нет

Тесты подобрали, что дальше? Правильно, запускаем:

Тест номер 1
> python.exe example.py
Пожалуйста, введите три числа через пробел: 10 0 0
Корень номер 0 равен 0.00

Тест номер 2:
> python.exe example.py
Пожалуйста, введите три числа через пробел:  2 5 -3
Корень номер 1 равен 0.50
Корень номер 2 равен -3.00

Тест номер 3:
> python.exe example.py
Пожалуйста, введите три числа через пробел: 10 0 2
Traceback (most recent call last):
  File "C:PyProjectstprogerexample.py", line 32, in <module>
    main()
  File "C:PyProjectstprogerexample.py", line 27, in main
    result = square_eq_solver(a, b, c)
  File "C:PyProjectstprogerexample.py", line 11, in square_eq_solver
    result.append((-b + sqrt(discriminant)) / (2 * a))
ValueError: math domain error

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

def square_eq_solver(a, b, c):
   result = []
   discriminant = b * b - 4 * a * c

   if discriminant == 0:
       result.append(-b / (2 * a))
   elif discriminant > 0:  # <--- изменили условие, теперь
                           # при нулевом дискриминанте
                           # не будут вычисляться корни
       result.append((-b + sqrt(discriminant)) / (2 * a))
       result.append((-b - sqrt(discriminant)) / (2 * a))

   return result

Запускаем все тесты повторно и они срабатывают нормально.

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

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

Две самые популярные библиотеки — unittest и pytest. Попробуем каждую, чтобы объективно оценить синтаксис.

Начнем с unittest, потому что именно с нее многие знакомятся с миром тестирования. Причина проста: библиотека по умолчанию встроена в стандартную библиотеку языка Python.

Формат кода

По формату написания тестов она сильно напоминает библиотеку JUnit, используемую  в языке Java для написания тестов:

  • тесты должны быть написаны в классе;
  • класс должен быть отнаследован от базового класса unittest.TestCase;
  • имена всех функций, являющихся тестами, должны начинаться с ключевого слова test;
  • внутри функций должны быть вызовы операторов сравнения (assertX) — именно они будут проверять наши полученные значения на соответствие заявленным.

Пример использования unittest для нашей задачи

import unittest

class SquareEqSolverTestCase(unittest.TestCase):
   def test_no_root(self):
       res = square_eq_solver(10, 0, 2)
       self.assertEqual(len(res), 0)

   def test_single_root(self):
       res = square_eq_solver(10, 0, 0)
       self.assertEqual(len(res), 1)
       self.assertEqual(res, [0])

   def test_multiple_root(self):
       res = square_eq_solver(2, 5, -3)
       self.assertEqual(len(res), 2)
       self.assertEqual(res, [0.5, -3])

Запускается данный код следующей командой

python.exe -m unittest example.py

И в результате на экран будет выведено:

> python.exe -m unittest example.py
...
------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

В случае, если в каком-нибудь из тестов будет обнаружена ошибка, unittest не замедлит о ней сообщить:

> python.exe -m unittest example.py
F..
==================================================================
FAIL: test_multiple_root (hello.SquareEqSolverTestCase)
------------------------------------------------------------------
Traceback (most recent call last):
  File "C:PyProjectstprogerexample.py", line 101, in test_multiple_root
    self.assertEqual(len(res), 3)
AssertionError: 2 != 3
------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

Unittest: аргументы “за”

  • Является частью стандартной библиотеки языка Python: не нужно устанавливать ничего дополнительно;
  • Гибкая структура и условия запуска тестов. Для каждого теста можно назначить теги, в соответствии с которыми будем запускаться либо одна, либо другая группа тестов;
  • Быстрая генерация отчетов о проведенном тестировании, как в формате plaintext, так и в формате XML.

Unittest: аргументы “против”

  • Для проведения тестирования придётся написать достаточно большое количество кода (по сравнению с другими библиотеками);
  • Из-за того, что разработчики вдохновлялись форматом библиотеки JUnit, названия основных функций написаны в стиле camelCase (например setUp и assertEqual);
  • В языке python согласно рекомендациям pep8 должен использоваться формат названий snake_case (например set_up и assert_equal).

Pytest

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

Pytest позволяет провести модульное тестирование (тестирование отдельных компонентов программы), функциональное тестирование  (тестирование способности кода удовлетворять бизнес-требования), тестирование API (application programming interface) и многое другое.

Формат кода

Написание тестов здесь намного проще, нежели в unittest. Вам нужно просто написать несколько функций, удовлетворяющих следующим условиям:

  • Название функции должно начинаться с ключевого слова test;
  • Внутри функции должно проверяться логическое выражение при помощи оператора assert.

Пример использования pytest для нашей задачи:

def test_no_root():
   res = square_eq_solver(10, 0, 2)
   assert len(res) == 0

def test_single_root():
   res = square_eq_solver(10, 0, 0)
   assert len(res) == 1
   assert res == [0]

def test_multiple_root():
   res = square_eq_solver(2, 5, -3)
   assert len(res) == 3
   assert res == [0.5, -3]

Запускается данный код следующей командой

pytest.exe example.py

И в результате на экран будет выведено:

> pytest.exe example.py
======================= test session starts ======================
platform win32 -- Python 3.9.6, pytest-7.1.2, pluggy-1.0.0
rootdir: C:PyProjectstproger
collected 3 items

example.py ...                                              [100%]

======================== 3 passed in 0.03s =======================

В случае ошибки вывод будет несколько больше:

> pytest.exe example.py
======================= test session starts ======================
platform win32 -- Python 3.9.6, pytest-7.1.2, pluggy-1.0.0
rootdir: C:PyProjectstproger
collected 3 items

example.py ..F                                              [100%]

============================ FAILURES ============================
_______________________ test_multiple_root _______________________

    def test_multiple_root():
        res = square_eq_solver(2, 5, -3)
>       assert len(res) == 3
E       assert 2 == 3
E        +  where 2 = len([0.5, -3.0])

example.py:116: AssertionError
===================== short test summary info ====================
FAILED example.py::test_multiple_root - assert 2 == 3

=================== 1 failed, 2 passed in 0.10s ==================

Pytest: аргументы “за”

  • Позволяет писать компактные (по сравнению с unittest) наборы тестов;
  • В случае возникновения ошибок выводится гораздо больше информации о них;
  • Позволяет запускать тесты, написанные для других тестирующих систем;
  • Имеет систему плагинов (и сотни этих самых плагинов), расширяющую возможности фреймворка. Примеры таких плагинов: pytest-cov, pytest-django, pytest-bdd;
  • Позволяет запускать тесты в параллели (при помощи плагина pytest-xdist).

Pytest: аргументы “против”

  • pytest не входит в стандартную библиотеку языка Python. Поэтому его придётся устанавливать отдельно при помощи команды pip install pytest;
  • совместимость кода с другими фреймворками отсутствует. Так что, если напишете код под pytest, запустить его при помощи встроенного unittest не получится.

Ну и что лучше?

  • Если вам нужно базовое юнит-тестирование и вы знакомы с фреймворками вида xUnit, тогда вам подойдёт unittest.
  • Если нужен фреймворк, позволяющий создавать краткие и изящные тесты, реализующие сложную логику проверок, то pytest.

Post Scriptum

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

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

Пример фреймворка для автоматизации тестирования на Python


Инструменты

  • Pytest — тестовый фреймворк для создания тестов и их запуска
  • Selenium — инструмент для автоматизации действий веб-браузера
  • Selenoid — легковесный аналог Selenium Hub написанный на Golang.Позволяет запускать тесты внутри Docker контейнеров.
  • Allure Report — Система отчетности (Тест репорт система)
  • Docker — программное обеспечение для автоматизации развёртывания и управления приложениями в средах с поддержкой контейнеризации.

Описание структуры проекта

  • config — Каталог для конфигурационных файлов.В нем находится browsers.json который содержит описание браузеров и их версий для работы с Selenoid.
  • core — Каталог для хранения объектов,классов и т.д. которые используются как слой инициализации для WebDriver.
  • helpers — Методы для взаимодействия с браузером через протокол WebDriver .
  • page — Слой описания page элементов и page object.
  • tests — Слой описания бизнес логики сценариев и их проверки.
  • utils — Каталог для хранения вспомогательных утилит

Способы запуска автотестов


Способ №1. Запуск автотестов внутри контейнера.

  1. Запускаем shell скрипт sh up.sh. Это запустит все необходимые контейнеры для работы и создаcт shell скрипт,в котором описаны команды для загрузки образов Selenoid с готовыми браузерами.

Selenoid-UI станет доступен по адресу http://localhost:8080/#/

Allure Report UI доступен по адресу http://localhost:5252/allure-docker-service-ui/projects/default

  1. Скрипт автоматически скачает необходимые image с браузерами, которые будут использоваться для создания контейнеров.Скрипт собирает default версии браузеров, указанных в config/browsers.json
  2. Вводим в консоль команду docker exec -it ui-autotests <ваша_команда>,где вместо <ваша команда> подставляем неообходимую команду для тест-комплекта. Как пример: docker exec -it ui-autotests pytest
    На http://localhost:8080/#/ вы сможете увидеть запущенные контейнеры с браузерами, в которых запущены тестовые сценарии

Посмотреть что происходит в контейнере можно, нажав на его идентификатор


4. После прогона автотестов, результаты можно посмотреть по адресу http://localhost:5252/allure-docker-service-ui/projects/default/ либо получить последний отчет — http://localhost:5252/allure-docker-service-ui/projects/default/reports/latest

Способ №2. Запуск автотестов локально

  1. Устанавливаем локально Python. Подробнее на https://www.python.org/downloads/
  2. В папке проекта запускаем команду python -m pip install --upgrade pip для обновления pip
  3. В папке проекта запуска команду pip3 install -r requirements.txt для установки зависимостей
  4. Загружаем образ с браузером. Допустим docker pull selenoid/chrome:106.0
  5. Вводим команду для запуска автотестов. Допустим python pytest
  6. После прогона автотестов, результаты можно посмотреть по адресу http://localhost:5252/allure-docker-service-ui/

Опции

  • В файле pytest.ini прописаны доп.опции.
    • pytest-xdist и его параметр -n X, где X — количество запущенных тестов одновременно, параллельно друг другу.

Понравилась статья? Поделить с друзьями:
  • Как пишутся sql запросы
  • Как пишутся apk файлы
  • Как пишут японцы слева направо или справа налево
  • Как пишут японцы сверху вниз или слева направо
  • Как пишут японцы сверху вниз или вдоль