Как стать автором
Обновить
167.74
Beget
Beget — международный облачный провайдер

Параметризация: PyTest vs RobotFramework

Уровень сложностиПростой
Время на прочтение19 мин
Количество просмотров735

В этой статье вы можете найти наглядное сравнение двух популярных инструментов для автоматизации тестирования: PyTest и RobotFramework.

На Хабре уже есть хорошая статья с общим сравнением этих фреймворков. Я сфокусируюсь на простоте параметризации тестов. 

alt

В одной из хабр-статей про Robot Framework утверждалось, что там нельзя создавать тесты "на лету" как в PyTest.

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

Начать, тем не менее предлагаю со сценариев, в которых Robot Framework и PyTest справляются одинаково хорошо. Фреймворком PyTest я пользовался только в образовательных целях, но в этой статье с ним всё максимально просто. Особых знаний PyTest не нужно, так как напрягаться чтобы повторить те же тесты приходится Robot Framework-y.

Параметризация

Если несколько тестов проверяют одну и ту же функцию передавая туда разные аргументы это выглядит довольно громоздко: 

Тест1
Проверим что Сложение(0, 0) возвращает 0
Тест2
Проверим что Сложение(-1, 1) возвращает 0
…
Тест9
Проверим что Сложение(7, 8) возвращает 15

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

alt
alt

 

Это обычно и понимают под параметризацией.

Выглядит такой тест примерно так:

ПараметризированныйТест((0, 0, 0), (-1, 1, 0), ... , (7, 8, 15))

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

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

Помимо лаконичности кода у параметризации есть ряд преимуществ разной степени уникальности.

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

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

Подготовка тестового окружения

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

Сперва нужно создать виртуальное окружение.

python -m venv venv

Затем нужно активировать его.

В Windows команда выглядит следующим образом:

.\venv\Scripts\activate

В Linux — немного по‑другому:

source ./venv/bin/activate

Установить зависимости можно следующей командой:

python -m pip install pytest requests xmlschema robotframework robotframework-datadriver

Как вариант можно установить зависимости из requirements.txt

pytest==8.3.5
requests==2.32.3
xmlschema==3.4.3
robotframework==7.2.2
robotframework-datadriver==1.11.22
python -m pip install -r requirements.txt

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

Пример 1. Трехзначные числа

Функция filter_three() из модуля three_digit.py должна возвращать True если число трехзначное.

def filter_three(x: int) -> bool: return 100 <= x < 1000

Протестируем её с помощью PyTest и RF.

Структура проекта:

filter_three/ 
├── three_digit.py 
└── tests 
    ├── pytest
    |   └── test_filter_three.py
    └── robot 
        └── test_filter_three.robot

Когда мы проверяем функцию filter_three() мы будем передавать в нее как трехзначные так и не трёхзначные числа.

С каждым таким тестом придётся передавать и ожидаемый результат.

Пример 1. PyTest

Наши тестовые данные можно представить как список кортежей вида:

[ 
    (Число, Ожидаемый результат),
    (Число, Ожидаемый результат),
    (Число, Ожидаемый результат)
]

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

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

import pytest
from three_digit import filter_three


@pytest.mark.parametrize("test_input, expected_result", [
    (8, False),
    (99, False),
    (100, True),
    (101, True),
    (999, True),
    (1000, False)
])

def test_compare(test_input, expected_result):
    res = filter_three(test_input)
    assert res == expected_result
python -m pytest -v --no-header .\tests\pytest\test_filter_three.py
======================== test session starts ==========================
collected 6 items
tests/pytest/test_filter_three.py::test_compare[9-False] PASSED [ 16%]
tests/pytest/test_filter_three.py::test_compare[99-False] PASSED [ 33%]
tests/pytest/test_filter_three.py::test_compare[100-True] PASSED [ 50%]
tests/pytest/test_filter_three.py::test_compare[101-True] PASSED [ 66%]
tests/pytest/test_filter_three.py::test_compare[999-True] PASSED [ 83%]
tests/pytest/test_filter_three.py::test_compare[1000-False] PASSED [100%]
========================= 6 passed in 0.02s ===========================

Для более наглядной демонстрации результатов можно дать каждому тесту название, с помощью pytest.param id=

Это упростит сравнение с роботом, поэтому приём будет использоваться во всех тестах этой статьи.

import pytest
from three_digit import filter_three


@pytest.mark.parametrize("test_input, expected_result", [
    pytest.param(8, False, id="Digit"),
    pytest.param(99, False, id="Two Digit Number"),
    pytest.param(100, True, id="Lower Border"),
    pytest.param(101, True, id="Three Digit Number"),
    pytest.param(999, True, id="Upper Border"),
    pytest.param(1000, False, id="Four Digit Number")
])

def test_compare(test_input, expected_result):
    res = filter_three(test_input)
    assert res == expected_result
======================== test session starts ==========================
collected 6 items 
tests/pytest/test_filter_three.py::test_compare[Digit] PASSED [ 16%]
tests/pytest/test_filter_three.py::test_compare[Two Digit Number] PASSED [ 33%]
tests/pytest/test_filter_three.py::test_compare[Lower Border] PASSED [ 50%]
tests/pytest/test_filter_three.py::test_compare[Three Digit Number] PASSED [ 66%]
tests/pytest/test_filter_three.py::test_compare[Upper Border] PASSED [ 83%]
tests/pytest/test_filter_three.py::test_compare[Four Digit Number] PASSED [100%]
========================= 6 passed in 0.02s ===========================

Пример 1. Robot Framework

В Robot Framework параметризация делается с помощью шаблонов.

*** Settings ***

Library  ../../three_digit.py

Test Template    Validate Number

*** Test Cases ***    number    expected_result
Digit                 8         ${False}
Two Digit Number      99        ${False}
Lower Border          100       ${True}
Three Digit Number    101       ${True}
Upper Border          999       ${True}
Four Digit Number     1000      ${False}

*** Keywords ***
Validate Number
   [Arguments]    ${number}    ${expected}
   ${res}=    Filter Three    ${number}
   Should Be Equal     ${res}    ${expected}
python -m robot .\tests\robot\test_filter_three.robot
========================================================================
====== Test Filter Three 
========================================================================
====== Digit | PASS |
------------------------------------------------------------------------
------ Two Digit Number | PASS |
------------------------------------------------------------------------
------ Lower Border | PASS |
------------------------------------------------------------------------
------ Three Digit Number | PASS |
------------------------------------------------------------------------
------ Upper Border | PASS |
------------------------------------------------------------------------
------ Four Digit Number | PASS |
------------------------------------------------------------------------
------ Test Filter Three | PASS | 6 tests, 6 passed, 0 failed
========================================================================

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

Пример 2. Умножение двух чисел

Следующий пример — параметризации простого умножения. Он не очень сильно отличается от предыдущего. Разница в том, что здесь больше аргументов передается в тест. Изучая сперва Пример 1 а затем Пример 2 читателю, не знакомому с PyTest, проще разобраться с синтаксисом.

Если вы хорошо знакомы с PyTest можете переходить к следующему примеру.

Структура проекта:

parametrize/ 
├── prod 
│ ├── prod.py 
│ └── tests 
│     ├── pytest 
│     │   └── test_prod.py 
│     └── robot 
│         └── test_prod.robot 
└── venv
# prod.py
def prod(a, b):
    return a * b

Пример 2. PyTest

Здесь тестовые данные будут немного сложнее. Мы передаем список кортежей. В каждом кортеже находится два элемента. Первый это кортеж из двух умножаемых чисел. Второй — ожидаемый результат в формате int.

[ 
    ((Множитель, Множитель), Ожидаемый результат),
    ((Множитель, Множитель), Ожидаемый результат),
    ((Множитель, Множитель), Ожидаемый результат)
]

Чтобы не накручивать сложную распаковку, элементы кортежа с множителями перебираются с помощью *args.

# test_prod.py
import pytest
from prod import prod


@pytest.mark.parametrize(
    "args, expected_result",
    [
       pytest.param((0, 0), 0, id="zero - zero"),
        pytest.param((7, -8), -56, id="positive - negative"),
       pytest.param((13.0, 14), 182, id="float positive - positive"),
    ])

def test_param_prod(args, expected_result):
    res = prod(*args)
    assert res == expected_result

Запускать тесты будем из директории prod командой.

python -m pytest -v --no-header .\tests\pytest\
====================== test session starts ======================
collected 3 items
tests/pytest/test_prod.py::test_basic_param_prod[zero - zero] PASSED [ 33%]
tests/pytest/test_prod.py::test_basic_param_prod[positive - negative] PASSED [ 66%]
tests/pytest/test_prod.py::test_basic_param_prod[float positive - positive] PASSED [100%] 
==================== 3 passed in 0.01s =========================

Пример 2. Robot Framework

*** Settings ***

Library  ../../prod.py
Test Template    Validate Prod

*** Test Cases ***         arg1       arg2     expected_result
Zero Zero                  ${0}       ${0}     ${0}
Positive Negative          ${7}       ${-8}    ${-56}
Float Positive Positive    ${13.0}    ${14}    ${182}


*** Keywords ***
Validate Prod
    [Arguments]    ${arg1}    ${arg2}    ${expected_result}
    ${product}=    Prod    ${arg1}    ${arg2}
    Should Be Equal    ${product}    ${expected_result}
python -m robot .\tests\robot\test_prod.robot
==================================================
====== Test Prod
==================================================
====== Zero Zero | PASS |
--------------------------------------------------
------ Positive Negative | PASS |
--------------------------------------------------
------ Float Positive Positive | PASS |
--------------------------------------------------
------ Test Prod | PASS | 3 tests, 3 passed, 0 failed
==================================================

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

Пример 3. Квадратное уравнение

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

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

parametrize/ 
├── quad 
│   ├── quadratic.py 
│   └── tests 
│       ├── pytest 
│       │   └── test_quadratic.py 
│       └── robot 
│           └── test_quadratic.robot 
└── venv

Запускать тесты будет из директории quad.

Пример 3.0 PyTest

Рассмотрим полный код теста:

# test_quadratic.py
import pytest
from quadratic import quadratic_solve


@pytest.mark.parametrize("args, expected_result", [
    pytest.param((1, -3, -4), [4, -1], id="two roots",),
    pytest.param((1, -2, 1), [1.0, None], id="single root",),
    pytest.param((0, 0, 0), [None, None], id="no roots",)
])

def test_solution(args, expected_result):
    res = quadratic_solve(*args)
    assert res == expected_result
python -m pytest --no-header -v tests/pytest/test_quadratic.py
============================ test session starts =============================== 
collected 5 items 
tests/test_quadratic.py::test_raises_type_error PASSED [ 20%]
tests/test_quadratic.py::test_result_is_tuple PASSED [ 40%]
tests/test_quadratic.py::test_solution[two roots] PASSED [ 60%]
tests/test_quadratic.py::test_solution[single root] PASSED [ 80%]
tests/test_quadratic.py::test_solution[no roots] PASSED [100%]
============================== 5 passed in 0.01s ==============================

Как можно увидеть при использовании опции ‑v все три теста [two roots], [single root], [no roots] успешно пройдены.

Пример 3.0 RobotFramework

Параметризация по‑прежнему будет осуществляться с помощью шаблонов.

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

# test_quadratic_lists.robot
*** Settings ***

Library  ../../quadratic_lists.py

Test Template    Verify Solution


*** Variables ***
@{roots1}=    ${4.0}    ${-1.0}
@{roots2}=    ${1.0}    ${None}
@{roots3}=    ${None}    ${None}

*** Test Cases ***    a       b        c        expected_result
Two Roots             ${1}    ${-3}    ${-4}    ${roots1}
Single Root           ${1}    ${-2}    ${1}     ${roots2}
No Roots              ${0}    ${0}     ${0}     ${roots3}

*** Keywords ***
Verify Solution
    [Arguments]    ${a}    ${b}    ${c}    ${expected}
    ${res}=    Quadratic Solve    ${a}    ${b}    ${c}
    Should Be Equal     ${res}    ${expected}
python -m robot .\tests\robot\test_quadratic_lists.robot
========================================================================
====== Test Quadratic Lists
========================================================================
====== Two Roots | PASS |
------------------------------------------------------------------------
------ Single Root | PASS |
------------------------------------------------------------------------
------ No Roots | PASS |
------------------------------------------------------------------------
------ Test Quadratic Lists | PASS | 3 tests, 3 passed, 0 failed 
========================================================================

Поменяем тип возвращаемого значения на кортеж

Пример 3.1 PyTest

Всё, что нужно изменить в PyTest коде — квадратные скобки на круглые. [4, -1] на (4, -1) и так далее. В нашем примере это замена шести символов.

# test_quadratic.py
import pytest
from quadratic import quadratic_solve


@pytest.mark.parametrize("args, expected_result", [
    pytest.param((1, -3, -4), (4, -1), id="two roots",),
    pytest.param((1, -2, 1), (1.0, None), id="single root",),
    pytest.param((0, 0, 0), (None, None), id="no roots",)
])

def test_solution(args, expected_result):
    res = quadratic_solve(*args)
    assert res == expected_result

Пример 3.1 RobotFramework

Роботу сложнее приспособиться к кортежам.

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

*** Settings ***

Library  ../../quadratic.py

Test Template    Verify Solution
Suite Setup    Prepare Variables


*** Variables ***
@{roots1}=    ${4.0}    ${-1.0}
@{roots2}=    ${1.0}    ${None}
@{roots3}=    ${None}    ${None}


*** Test Cases ***    a       b        c        expected_result
Two Roots             ${1}    ${-3}    ${-4}    ${ex_res1}
Single Root           ${1}    ${-2}    ${1}     ${ex_res2}
No Roots              ${0}    ${0}     ${0}     ${ex_res3}


*** Keywords ***
Verify Solution
    [Arguments]    ${a}    ${b}    ${c}    ${expected}
    ${res}=    Quadratic Solve    ${a}    ${b}    ${c}
    Should Be Equal     ${res}    ${expected}


Convert List To Tuple
    [Arguments]    ${list}
    ${tuple}=    Evaluate    tuple(${list})
    RETURN    ${tuple}


Prepare Variables
    ${tup1}=   Convert List To Tuple    ${roots1}
    ${tup2}=   Convert List To Tuple    ${roots2}
    ${tup3}=   Convert List To Tuple    ${roots3}
    Set Suite Variable    ${ex_res1}    ${tup1}
    Set Suite Variable    ${ex_res2}    ${tup2}
    Set Suite Variable    ${ex_res3}    ${tup3}

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

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

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

Функция get_digits() из модуля list_of_digits.py должна возвращать произвольный набор цифр.

Повторы разрешены.

Цифры это 0, 1, 2… 9.

Функция написана с ошибкой, но её нелегко поймать одиночным тестом.

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

С помощью PyTest можно легко получить заранее неопределённое количество тестов. А если нужно и случайное. Как в нашем примере.

Структура проекта:

list_of_digits/ 
├── list_of_digits.py 
├── tests 
│   ├── pytest 
│   │   └── test_list_of_digits.py 
│   └── robot 
│       └── test_list_of_digits.robot 
└── venv
# list_of_digits.py
import random

def get_digits() -> list:
    n = random.randint(1, 10)
    result = []
    for i in range(0, n):
        result.append(random.randint(4, 10))
    return result

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

Пример 4. PyTest

Рассмотрим полный код теста:

# test_list_of_digits.py
import pytest
import random
from list_of_digits import get_digits


digits = get_digits()

@pytest.mark.parametrize("digit", digits)
def test_if_digit(digit):
    assert 0 <= digit < 10

Запустим тесты из директории list_of_digits несколько раз пока не поймаем ошибку.

python -m pytest -v --no-header .\tests\pytest\test_list_of_digits.py
=============== test session starts =============== 
collected 2 items 
tests/pytest/test_list_of_digits.py::test_if_digit[8] PASSED [ 50%]
tests/pytest/test_list_of_digits.py::test_if_digit[7] PASSED [100%]
================ 2 passed in 0.01s ================

=============== test session starts =============== 
collected 10 items
tests/pytest/test_list_of_digits.py::test_if_digit[9_0] PASSED [ 10%]
tests/pytest/test_list_of_digits.py::test_if_digit[4] PASSED [ 20%]
tests/pytest/test_list_of_digits.py::test_if_digit[7] PASSED [ 30%]
tests/pytest/test_list_of_digits.py::test_if_digit[8_0] PASSED [ 40%]
tests/pytest/test_list_of_digits.py::test_if_digit[9_1] PASSED [ 50%]
tests/pytest/test_list_of_digits.py::test_if_digit[8_1] PASSED [ 60%]
tests/pytest/test_list_of_digits.py::test_if_digit[6_0] PASSED [ 70%]
tests/pytest/test_list_of_digits.py::test_if_digit[8_2] PASSED [ 80%] 
tests/pytest/test_list_of_digits.py::test_if_digit[10] FAILED [ 90%]
tests/pytest/test_list_of_digits.py::test_if_digit[6_1] PASSED [100%]
===================== FAILURES =====================

В первом тест‑ране был создан список из двух элементов. Его проверка не выявила ошибок.

Благодаря @pytest.mark.parametrize каждая проверка была выполнена как отдельный тест. Поэтому мы видим два результата PASSED.

Во втором тест‑ране список был уже из десяти элементов. Мы проверили каждый элемент из списка.

Девятый элемент оказался равен 10, а это не цифра. Поэтому тест упал.

Из этого теста хорошо видно, что PyTest делает с аргументами, переданными в декоратор.

@pytest.mark.parametrize("digit", digits)

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

Я специально сделал пример с простейшей итерацией обычной переменной по списку.

Без PyTest мы могли бы написать следующую проверку:

for digit in digits:
    assert 0 <= digit <= 9, f"{digit} is not a digit"

Но она бы падала при первой ошибке а с PyTest мы видим все падения.

Пример 4. RobotFramework

Как лаконично решить эту задачу на роботе пока непонятно. Ниже предлагаю решение с использованием DataDriver.

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

Установить его нужно отдельно, так как в стандартную библиотеку робота он не входит.

python -m pip install robotframework-datadriver

Для сохранения в .csv я сделал кастомную .py библиотеку. Скорее всего это можно сделать и на чистом роботе. Но суть не меняется — мы вынуждены добавить дополнительное действие в наш тест.

list_of_digits/ 
├── list_of_digits.py 
├── tests 
│   ├── pytest 
│   │   └── test_list_of_digits.py 
│   └── robot 
│       ├── libraries 
│       │   └── save_to_csv.py 
│       └── test_list_of_digits.robot 
└── venv

После запуска теста рядом с list_of_digits.py будет создан файл UserData.csv

# save_to_csv.py
def save_list_to_csv(list_of_digits):
    with open("UserData.csv", "w") as f:
        f.write("*** Test Cases ***;${var}")
    i = 0
    for digit in list_of_digits:
        i += 1
        with open("UserData.csv", "a") as f:
            f.writelines("\n")
            f.writelines("Test-" + str(i) + ". Digit: " + str(digit))
            f.writelines(";")
            f.writelines(str(digit))
# test_list_of_digits.py
*** Settings ***
Library  DataDriver    ../../UserData.csv
Library  ../../list_of_digits.py
Library  libraries/save_to_csv.py

Suite Setup    Prepare Variables

Test Template    Variable Is Digit

*** Test Cases ***
Test    ${var}

*** Keywords ***
Prepare Variables
    ${list_of_digits}=    Get Digits
    Save List To Csv    ${list_of_digits}

Variable Is Digit
    [Arguments]    ${var}
    ${num}=  Convert To Integer    ${var}
    Should Be True    ${num} >= 0
    Should Be True    ${num} < 10
python -m robot .\tests\robot\test_list_of_digits.robot
========================================================================
====== Test List Of Digits
========================================================================
====== Test-1. Digit: 9 | PASS |
------------------------------------------------------------------------
------ Test-2. Digit: 4 | PASS |
------------------------------------------------------------------------
------ Test-3. Digit: 7 | PASS |
------------------------------------------------------------------------
------ Test-4. Digit: 7 | PASS |
------------------------------------------------------------------------
------ Test List Of Digits | PASS | 4 tests, 4 passed, 0 failed
========================================================================
========================================================================
====== Test List Of Digits
========================================================================
====== Test-1. Digit: 8 | PASS |
------------------------------------------------------------------------
------ Test-2. Digit: 10 | FAIL | '10 < 10' should be true.
------------------------------------------------------------------------
------ Test-3. Digit: 1 | PASS |
------------------------------------------------------------------------
------ Test List Of Digits | FAIL | 3 tests, 2 passed, 1 failed
========================================================================

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

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

Если вы знаете более элегантный способ — пишите в комментариях.

Пример 5. Проверка сайта

Практический пример применения параметризации для проверки ответов страниц сайта. Именно при решении этой задачи я столкнулся с необходимостью создавать заранее неизвестное число тестов.

Задача проверить все ли страницы сайта testsetup.ru отвечают HTTP кодом 200.

Структура проекта:

from_site_map/ 
├── app 
│   ├── site_map.py 
│   └── tests 
│       ├── pytest 
│       │   └── test_by_site_map.py 
│       └── robot 
│           ├── libraries 
│           │ └── save_to_csv.py 
│           └── test_by_site_map.robot 
└── venv

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

Cкачиваем её в ту же директорию где лежит site_map.py:

from_site_map/ 
├── app 
│   ├── site_map.py 
│   ├── sitemap.xml 
│   └── tests 
│       ├── pytest 
│       │   └── test_by_site_map.py 
│       └── robot 
│           ├── libraries 
│           │   └── save_to_csv.py 
│           └── test_by_site_map.robot 
└── venv

Выглядит карта сайта следующим оригинальным образом:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
 <url>
   <loc>https://testsetup.ru/pytest/</loc>
   <priority>0.8</priority>
   <changefreq>daily</changefreq>
 </url>
 <url>
   <loc>https://testsetup.ru/robot/</loc>
   <priority>0.6</priority>
   <changefreq>daily</changefreq>
 </url>
</urlset>

Только страниц там гораздо больше. Теперь можно переходить к тестам.

Пример 5. PyTest

Для парсинга карты сайта будем использовать библиотеку xmlschema а для обращения к сайту — библиотеку requests.

# site_map.py
import xml.etree.ElementTree as ET


def get_urls() -> list:
    tree = ET.parse("sitemap.xml")
    root = tree.getroot()

    urls = []

    for child in root:
        for gc in child:
            tag = gc.tag
            if "loc" in tag:
                urls.append(gc.text)

    return urls

Тест будет очень лаконичным. За это мы и любим PyTest.

# test_by_site_map.py
import pytest
import requests
from site_map import get_urls


endpoints = get_urls()

@pytest.mark.parametrize('endpoint', endpoints)
def test_status(endpoint):
    r = requests.get(endpoint, allow_redirects=False)
    assert r.status_code == 200

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

python -m pytest -v --no-header tests\pytest\test_by_site_map.py
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/auto/] PASSED [ 0%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/theory/] PASSED [ 0%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/robot/] PASSED [ 0%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/pytest/] PASSED [ 1%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/testcomplete/] PASSED [ 1%]
...
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/edu/ PASSED [ 99%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/selenium/] PASSED [100%]
=================================== 553 passed in 121.11s (0:02:01) ====================================

Пример 5. RobotFramework

Для парсинга карты сайта будем использовать библиотеку XML а для обращения к сайту — библиотеку RequestsLibrary.

*** Settings ***
Library    XML
Library    RequestsLibrary
Library    Collections
Library    DataDriver    ../../Urls.csv
Library    libraries/save_to_csv.py

Suite Setup    Prepare Variables
Test Template    Status Is OK

*** Variables ***
${XML}=    sitemap.xml
@{urls}

*** Test Cases ***
Test ${url}

*** Keywords ***
Prepare Variables
    ${urls}=    Get Urls
    Save List To Csv    ${urls}

Status Is OK
    [Arguments]    ${url}
    ${response}=    GET    ${url}    expected_status=200    allow_redirects=${False}

Get Urls
    @{children} =   Get Elements  ${XML}  */loc
    FOR     ${child}    IN   @{children}
        Append To List    ${urls}    ${child.text}
    END
    RETURN    ${urls}
# save_to_csv.py
def save_list_to_csv(list_of_urls, file_name="Urls.csv"):
    with open(file_name, "w") as f:
        f.write("*** Test Cases ***;${url}")
    i = 0
    for url in list_of_urls:
        i += 1
        with open(file_name, "a") as f:
            f.writelines("\n")
            f.writelines("Test-" + str(i) + ". URL: " + str(url))
            f.writelines(";")
            f.writelines(str(url))
python -m robot .\tests\robot\test_site_map.robot
========================================================================
====== Test Site Map
========================================================================
====== 
------------------------------------------------------------------------
------ Test-1. URL: https://testsetup.ru/testcomplete/ | PASS |
------------------------------------------------------------------------
------ Test-2. URL: https://testsetup.ru/edu/ | PASS |
... 
------------------------------------------------------------------------
------ Test Site Map | PASS | 553 tests, 553 passed, 0 failed
========================================================================

Если показать тесты менеджеру, он увидит, что на входе у нас sitemap.xml.

А на выходе:

tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/robot/tags/] PASSED [ 0%]
...
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/selenium/] PASSED [100%]
============================== 553 passed in 121.11s (0:02:01) ===============================

Против:

====== Test Site Map
========================================================================
======
------------------------------------------------------------------------
------ Test-1. URL: https://testsetup.ru/testcomplete/ | PASS |
------------------------------------------------------------------------
------ Test-2. URL: https://testsetup.ru/edu/ | PASS |
...
------------------------------------------------------------------------
------ Test Site Map | PASS | 553 tests, 553 passed, 0 failed
========================================================================

Возможные проблемы

Если Python не находит модуль с кодом, можно попробовать перейти в директорию, родительскую по отношению к tests — в нашем последнем примере это from_site_map и добавить ее в системный путь.

В PowerShell команда будет выглядеть так:

$Env:Path += ";$pwd"

В Bash немного по‑другому.

export PATH=$PATH:$(pwd)

Заключение

Robot Framework хорошо справляется с параметризацией тестов, если их число заранее известно. У меня нет однозначного ответа какой инструмент лучше для таких задач.

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

Теги:
Хабы:
+6
Комментарии7

Публикации

Информация

Сайт
beget.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия