Ровно столько Python, сколько нужно, чтобы читать и писать тесты
requirements.txtPython — с python.org, версия 3.10 или новее. В Windows при установке отметьте «Add python.exe to PATH»; запускается командой py. В Linux Python уже есть, команда — python3. VS Code — с code.visualstudio.com, в нём расширение Python от Microsoft (вкладка Extensions, Ctrl+Shift+X).
Виртуальное окружение (venv) — папка .venv внутри проекта со своей копией Python и своими пакетами. Зачем: у каждого проекта свои версии pytest и Playwright, и они не мешают друг другу и системе. Окружение не переносят и не кладут в git — его создают заново командой.
| Действие | Windows | Linux |
|---|---|---|
| Создать окружение | py -m venv .venv | python3 -m venv .venv |
| Включить | .venv\Scripts\activate | source .venv/bin/activate |
| Признак, что включено | В начале строки приглашения — (.venv); команда python — уже из окружения | |
| Поставить пакеты | pip install -r requirements.txt | |
| Выключить | deactivate | |
Файл requirements.txt перечисляет пакеты с точными версиями — так у всей группы и в CI одинаковый набор:
# Пакеты курса «QA: автоматизация тестирования» — nevabit.ru/qa-automation/
# Установка: pip install -r requirements.txt, затем: playwright install chromium
pytest==9.2.0
requests==2.34.2
jsonschema==4.26.0
playwright==1.64.0
pytest-playwright==0.10.0
pytest-html==4.2.0
coverage==7.17.0
Проверить, что стоит, — pip list. Ошибка в имени модуля при импорте выглядит так — это самая частая причина «у меня не запускается»:
$ pip list | grep -iE "^(pytest|requests|playwright|jsonschema) "
jsonschema 4.26.0
playwright 1.64.0
pytest 9.2.0
requests 2.34.2
$ python -c "import requests; print(requests.__version__)"
2.34.2
$ python -c "import reqests"
Traceback (most recent call last):
File "<string>", line 1, in <module>
import reqests
ModuleNotFoundError: No module named 'reqests'
В VS Code: File → Open Folder — папка проекта; внизу справа выберите интерпретатор из .venv (или Ctrl+Shift+P → «Python: Select Interpreter»). Тогда встроенный терминал (Ctrl+`) сразу открывается с включённым окружением.
Переменная получает значение знаком =, тип объявлять не нужно. Для тестов важнее всего строки: названия, сообщения об ошибках, тексты страниц.
title = " Купить картридж "
priority = 2
done = False
clean = title.strip()
print(clean)
print(len(title), len(clean))
print(f"Задача «{clean}», приоритет {priority}")
print(clean.upper())
print("картридж" in clean)
print(clean[:6], clean[-8:])
print(type(priority), type(clean), type(done))
print(int("42") + 1, str(42) + "1")
$ python values.py
Купить картридж
19 15
Задача «Купить картридж», приоритет 2
КУПИТЬ КАРТРИДЖ
True
Купить картридж
<class 'int'> <class 'str'> <class 'bool'>
43 421
strip() обрезает пробелы по краям — так поступает и «Задачник» с названием задачи.f"…{переменная}…" — f-строка: значения подставляются в текст. В тестах так собирают адреса: f"/api/tasks/{task_id}".in — есть ли подстрока; [:6] и [-8:] — срезы, первые 6 и последние 8 символов."42" + 1 — ошибка: строку и число не складывают. Превращать — int() и str().Ответ API — почти всегда список словарей: список задач, у каждой поля id, title, priority. Поэтому эти два типа — главные в автотестах.
titles = ["Позвонить поставщику", "Обновить прайс-лист"]
titles.append("Заказать пропуск")
print(titles)
print(len(titles), titles[0], titles[-1])
task = {"id": 7, "title": "Обновить прайс-лист", "priority": 3, "due": None}
print(task["title"], task["priority"])
task["status"] = "new"
print(task.get("due"), task.get("owner", "нет поля"))
print(list(task.keys()))
tasks = [
{"title": "Отчёт", "priority": 1, "status": "done"},
{"title": "Звонок", "priority": 2, "status": "new"},
{"title": "Пропуск", "priority": 3, "status": "new"},
]
new_titles = [t["title"] for t in tasks if t["status"] == "new"]
print(new_titles)
print({t["title"]: t["priority"] for t in tasks})
$ python collections_demo.py
['Позвонить поставщику', 'Обновить прайс-лист', 'Заказать пропуск']
3 Позвонить поставщику Заказать пропуск
Обновить прайс-лист 3
None нет поля
['id', 'title', 'priority', 'due', 'status']
['Звонок', 'Пропуск']
{'Отчёт': 1, 'Звонок': 2, 'Пропуск': 3}
| Запись | Что делает |
|---|---|
titles[0], titles[-1] | Первый и последний элемент списка |
task["title"] | Значение по ключу; нет ключа — ошибка KeyError |
task.get("owner", "нет поля") | Значение или запасное, без ошибки |
[t["title"] for t in tasks if …] | Списковое включение: новый список из подходящих элементов. В тестах так достают названия из ответа |
{t["title"]: t["priority"] for t in tasks} | То же для словаря |
None — «значения нет». В JSON это null: у задачи без срока поле due равно None. Проверяют его через is None.
Блоки выделяются отступом в 4 пробела — это часть синтаксиса, а не оформление. Функция объявляется словом def, у параметров могут быть значения по умолчанию.
def check_priority(value, low=1, high=5):
"""Вернуть текст ошибки или None, если приоритет допустим."""
if not isinstance(value, int):
return "приоритет — целое число"
if value < low or value > high:
return f"приоритет от {low} до {high}"
return None
for value in [0, 1, 5, 6, "3"]:
error = check_priority(value)
if error is None:
print(value, "— допустимо")
else:
print(value, "—", error)
print(check_priority(7, high=10))
count = 0
while count < 3:
count += 1
print("повторов:", count)
$ python control.py
0 — приоритет от 1 до 5
1 — допустимо
5 — допустимо
6 — приоритет от 1 до 5
3 — приоритет — целое число
None
повторов: 3
isinstance(value, int) — проверка типа. Строка "3" выглядит как число, но числом не является — ровно такие случаи ищут негативные тесты.check_priority(7, high=10) — именованный аргумент меняет только нужный параметр.return или с пустым return возвращает None.def — описание функции (docstring). В тестах в ней пишут, что проверяется и по какому требованию.Исключение — сигнал «дальше выполнять нельзя». try … except перехватывает его, raise вызывает. Необработанное исключение останавливает программу и печатает трассировку — по ней ищут место ошибки:
def parse_priority(text):
try:
value = int(text)
except ValueError:
return None
return value
print(parse_priority("4"), parse_priority("четыре"))
def need_title(title):
if not title.strip():
raise ValueError("пустое название")
return title.strip()
try:
need_title(" ")
except ValueError as err:
print("ошибка:", err)
print(need_title(" Отчёт "))
need_title("")
$ python errors.py
4 None
ошибка: пустое название
Отчёт
Traceback (most recent call last):
File "~/qa/errors.py", line 24, in <module>
need_title("")
~~~~~~~~~~^^^^
File "~/qa/errors.py", line 14, in need_title
raise ValueError("пустое название")
ValueError: пустое название
Трассировку читают снизу вверх: последняя строка — тип и текст ошибки, выше — где она возникла (line 14, функция need_title) и откуда вызвана (line 24). Так же устроен отчёт pytest о падении теста — раздел 03.
pytest.raises (раздел 03).JSON — текстовый формат, в котором API передаёт данные. Модуль json превращает текст в словари и списки Python (loads) и обратно (dumps). pathlib.Path — путь к файлу с методами чтения и записи.
import json
from pathlib import Path
text = '{"id": 3, "title": "Обновить прайс-лист", "due": null, "overdue": false}'
task = json.loads(text)
print(task)
print(task["due"] is None, task["overdue"])
task["priority"] = 2
print(json.dumps(task, ensure_ascii=False))
path = Path("task.json")
path.write_text(json.dumps(task, ensure_ascii=False, indent=2), encoding="utf-8")
print(path.read_text(encoding="utf-8"))
loaded = json.loads(path.read_text(encoding="utf-8"))
print(loaded == task)
$ python data.py
{'id': 3, 'title': 'Обновить прайс-лист', 'due': None, 'overdue': False}
True False
{"id": 3, "title": "Обновить прайс-лист", "due": null, "overdue": false, "priority": 2}
{
"id": 3,
"title": "Обновить прайс-лист",
"due": null,
"overdue": false,
"priority": 2
}
True
| JSON | Python |
|---|---|
{"a": 1} объект | dict |
[1, 2] массив | list |
"текст" | str |
1, 2.5 | int, float |
true / false | True / False |
null | None |
ensure_ascii=False оставляет кириллицу читаемой, без него будет О…. encoding="utf-8" при работе с файлами указывайте всегда: в Windows по умолчанию другая кодировка, и русский текст превратится в «кракозябры».
Каждый файл .py — модуль. Общий код выносят в отдельный файл и импортируют: так в наборе тестов живут клиент API (client.py) и страницы-объекты (раздел 09).
"""Модуль с общими функциями: его импортируют другие файлы."""
STATUSES = {"new": "Новая", "in_progress": "В работе", "done": "Готово"}
def status_name(code):
return STATUSES.get(code, "неизвестный статус")
import helpers
from helpers import status_name
print(helpers.STATUSES["done"])
print(status_name("in_progress"), "/", status_name("closed"))
$ python main.py
Готово
В работе / неизвестный статус
import helpers — обращение через имя модуля: helpers.STATUSES. from helpers import status_name — имя функции напрямую. Python ищет модуль в папке запускаемого файла и в установленных пакетах; pytest в нашем проекте дополнительно ищет в корне проекта — это настройка pythonpath = . в pytest.ini.
Запуск файла — кнопка ▶ справа вверху или Ctrl+F5. Отладка — F5 → «Python Debugger: Current File». Перед этим поставьте точку останова: щелчок слева от номера строки, появится красная точка.
| Клавиша | Что делает в отладке |
|---|---|
| F10 | Выполнить строку, не заходя в функции |
| F11 | Зайти внутрь вызываемой функции |
| Shift+F11 | Выйти из функции |
| F5 | Продолжить до следующей точки останова |
Слева на панели «Run and Debug» видны значения переменных в момент остановки — это быстрее, чем расставлять print. Тесты отлаживают так же: на вкладке Testing (значок колбы) VS Code находит тесты pytest, а у каждого теста есть кнопка «Debug Test». Настроить: Ctrl+Shift+P → «Python: Configure Tests» → pytest → папка проекта.
.venv, пакеты ставят из requirements.txt с точными версиямиjson.loads / json.dumps, а у файлов — всегда encoding="utf-8"print