Урок за PyTest: Какво е PyTest? (с примери)
⚡ Умно обобщение
Пайтест е Python Тестова рамка, която ви помага да пишете прости, мащабируеми тестове за бази данни, API и потребителски интерфейси. Тя поддържа настройки, параметризация, маркери, паралелни изпълнения и подробно отчитане на твърдения, от основни модулни тестове до сложни функционални тестове.

Питест е една от най-широко използваните рамки за тестване в Python екосистема. Поддържа тестовия код кратък и четлив, открива тестовете автоматично и се разраства от първо твърдение до пълен набор от тестове за бази данни, API и потребителски интерфейси. Разделите по-долу разглеждат инсталацията, твърденията, настройките, маркерите, параметризацията и паралелните изпълнения с практически примери.
Какво е Pytest?
Питест е рамка за тестване, която ви позволява да пишете тестов код, използвайки Python език за програмиране. Той ви помага да пишете прости и мащабируеми тестови случаи за бази данни, API и дори потребителски интерфейси. Pytest се използва главно за писане на тестове за API и се мащабира от прости модулни тестове до сложни функционални тестове.
Защо да използваме Pytest?
Някои от предимствата на pytest са:
- Много лесно е да се започне с него, заради простия и изчистен синтаксис.
- Може да изпълнява тестове паралелно.
- Може да изпълни конкретен тест или подмножество от тестове.
- Автоматично открива тестове.
- Може да пропусне тестове.
- Отворен код.
Как да инсталирате Pytest
Следва процесът за инсталиране на pytest:
Стъпка 1) Можете да инсталирате pytest с командата по-долу.
pip install pytest==2.9.1
След като инсталацията приключи, можете да я потвърдите със следната команда.
py.test -h
Това ще покаже помощта.
Първи основен пример за Pytest
Сега ще научите как да използвате pytest с основен пример за pytest.
Създайте папка study_pytest. Ще създадете тестовите файлове в тази папка. Моля, отидете до тази папка в командния ред. Създайте файл с име test_sample1.py в папката.
Добавете кода по-долу в него и запазете.
import pytest def test_file1_method1(): x=5 y=6 assert x+1 == y,"test failed" assert x == y,"test failed" def test_file1_method2(): x=5 y=6 assert x+1 == y,"test failed"
Изпълнете теста, като използвате командата по-долу.
py.test
Ще получите резултата, както следва.
test_sample1.py F.
============================================== FAILURES ========================================
____________________________________________ test_sample1 ______________________________________
def test_file1_method1():
x=5
y=6
assert x+1 == y,"test failed"
> assert x == y,"test failed"
E AssertionError: test failed
E assert 5 == 6
test_sample1.py:6: AssertionError
Тук, в test_sample1.py F, буквата F означава неуспех, а точката (.) означава успех. В секцията с неуспехите можете да видите неуспешния(ите) метод(и) и реда на неуспеха. Тук x==y означава 5==6, което е невярно.
След това ще научите за твърденията (assertions) в pytest.
Твърдения в Pytest
Твърденията в Pytest са проверки, които връщат състояние True или False. В pytest, ако дадено твърдение е неуспешно в тестов метод, изпълнението на този метод се спира там. Останалият код в този тестов метод не се изпълнява и pytest продължава със следващия тестов метод.
Примери за твърдения на Pytest:
assert "hello" == "Hai" is an assertion failure. assert 4==4 is a successful assertion assert True is a successful assertion assert False is an assertion failure.
Помислете за следното.
assert x == y,"test failed because x=" + str(x) + " y=" + str(y)
Поставете този код в test_file1_method1() вместо твърдението по-долу.
assert x == y,"test failed"
Изпълнението на теста ще даде грешка като AssertionError: тестът е неуспешен x=5 y=6.
Как Pytest идентифицира тестовите файлове и методите за тестване
По подразбиране pytest идентифицира само имената на файловете, започващи с тест_ или завършва с _тест като тестовите файлове. Можем обаче да споменем изрично и други имена на файлове (обяснено по-късно). Pytest изисква имената на тестовите методи да започват с тестВсички други имена на методи се игнорират, дори ако изрично поискаме да ги изпълним.
Вижте някои примери за валидни и невалидни имена на pytest файлове.
test_login.py - valid login_test.py - valid testlogin.py -invalid logintest.py -invalid
Забележка: Да, можем изрично да поискаме от pytest да избере testlogin.py и logintest.py.
Вижте някои примери за валидни и невалидни методи за тестване на pytest.
def test_file1_method1(): - valid def testfile1_method1(): - valid def file1_method1(): - invalid
Забележка: Дори ако изрично споменем file1_method1(), pytest няма да изпълни този метод.
Изпълнете множество тестове от конкретен файл и множество файлове
В момента, в папката study_pytest, имаме файл test_sample1.py. Да предположим, че имаме няколко файла, например test_sample2.py и test_sample3.py. За да изпълним всички тестове от всички файлове в папката и подпапките, трябва само да изпълним командата pytest.
py.test
Това ще изпълни всички имена на файлове, започващи с test_, и имената на файловете, завършващи на _test, в тази папка и подпапките в тази папка.
За да стартираме тестове само от определен файл, можем да използваме py.test .
py.test test_sample1.py
Изпълнете подмножество от целия тест с Pytest
Понякога не искаме да изпълняваме целия набор от тестове. Pytest ни позволява да изпълняваме специфични тестове. Можем да го направим по два начина:
- Grouping на имената на тестовете чрез съвпадение на поднизове.
- Grouping на тестове с маркери.
Вече имаме test_sample1.py. Създайте файл test_sample2.py и добавете кода по-долу в него.
def test_file2_method1(): x=5 y=6 assert x+1 == y,"test failed" assert x == y,"test failed because x=" + str(x) + " y=" + str(y) def test_file2_method2(): x=5 y=6 assert x+1 == y,"test failed"
Така че в момента имаме следното.
• test_sample1.py • test_file1_method1() • test_file1_method2() • test_sample2.py • test_file2_method1() • test_file2_method2()
Вариант 1) Изпълняване на тестове чрез съпоставяне на поднизове
Тук, за да изпълним всички тестове, имащи method1 в името си, трябва да изпълним командата по-долу.
py.test -k method1 -v
-k <expression> is used to represent the substring to match
-v increases the verbosity
Така че, изпълнението на py.test -k method1 -v ще ви даде следния резултат.
test_sample2.py::test_file2_method1 FAILED
test_sample1.py::test_file1_method1 FAILED
============================================== FAILURES ==============================================
_________________________________________ test_file2_method1 _________________________________________
def test_file2_method1():
x=5
y=6
assert x+1 == y,"test failed"
> assert x == y,"test failed because x=" + str(x) + " y=" + str(y)
E AssertionError: test failed because x=5 y=6
E assert 5 == 6
test_sample2.py:5: AssertionError
_________________________________________ test_file1_method1 _________________________________________
@pytest.mark.only
def test_file1_method1():
x=5
y=6
assert x+1 == y,"test failed"
> assert x == y,"test failed because x=" + str(x) + " y=" + str(y)
E AssertionError: test failed because x=5 y=6
E assert 5 == 6
test_sample1.py:8: AssertionError
================================= 2 tests deselected by '-kmethod1' ==================================
=============================== 2 failed, 2 deselected in 0.02 seconds ===============================
Тук можете да видите, към края, 2 теста, премахнати от '-kmethod1', които са test_file1_method2 и test_file2_method2.
Опитайте да бягате с различни комбинации, като следните.
py.test -k method -v - will run all the four methods py.test -k methods -v – will not run any test as there is no test name matches the substring 'methods'
Вариант 2) Изпълнение на тестове по маркери
Pytest ни позволява да задаваме различни атрибути за тестовите методи, използвайки маркери на pytest, @pytest.mark. За да използваме маркери в тестовия файл, трябва да импортираме pytest в тестовите файлове. Тук ще приложим различни имена на маркери към тестовите методи и ще проведем специфични тестове въз основа на имената на маркерите. Можем да дефинираме маркерите за всяко име на тест, като използваме следното.
@pytest.mark.<name>.
Дефинираме маркери set1 и set2 в тестовите методи и ще изпълним теста, използвайки имената на маркерите. Актуализирайте тестовите файлове със следния код.
test_sample1.py
import pytest @pytest.mark.set1 def test_file1_method1(): x=5 y=6 assert x+1 == y,"test failed" assert x == y,"test failed because x=" + str(x) + " y=" + str(y) @pytest.mark.set2 def test_file1_method2(): x=5 y=6 assert x+1 == y,"test failed"
test_sample2.py
import pytest @pytest.mark.set1 def test_file2_method1(): x=5 y=6 assert x+1 == y,"test failed" assert x == y,"test failed because x=" + str(x) + " y=" + str(y) @pytest.mark.set1 def test_file2_method2(): x=5 y=6 assert x+1 == y,"test failed"
Можем да изпълним маркирания тест с командата по-долу.
py.test -m <name> -m <name> mentions the marker name
Изпълнете py.test -m set1. Това ще изпълни методите test_file1_method1, test_file2_method1 и test_file2_method2. Изпълнението на py.test -m set2 ще изпълни test_file1_method2.
Изпълнявайте тестове паралелно с Pytest
Обикновено един тестов пакет съдържа множество тестови файлове и стотици тестови методи, чието изпълнение отнема значително време. Pytest ни позволява да изпълняваме тестове паралелно. За целта първо трябва да инсталираме pytest-xdist, като изпълним командата по-долу.
pip install pytest-xdist
Сега можете да стартирате тестовете със следната команда.
py.test -n 4
Тук, -n изпълнява тестовете, използвайки множество работници. В горната команда ще има четири работници, които ще изпълняват тестовете.
Pytest тела
Фиксираните елементи (fixtures) се използват, когато искаме да изпълним някакъв код преди всеки тестов метод. Така че, вместо да повтаряме един и същ код във всеки тест, ние дефинираме фиксирани елементи (fixtures). Обикновено фиксираните елементи се използват за инициализиране на връзките към базата данни, предаване на базовия URLи т.н. Методът се маркира като pytest fixture, като се маркира със следното.
@pytest.fixture
Тестов метод може да използва pytest fixture, като го посочи като входен параметър. Създайте нов файл test_basic_fixture.py със следния код.
import pytest @pytest.fixture def supply_AA_BB_CC(): aa=25 bb =35 cc=45 return [aa,bb,cc] def test_comparewithAA(supply_AA_BB_CC): zz=35 assert supply_AA_BB_CC[0]==zz,"aa and zz comparison failed" def test_comparewithBB(supply_AA_BB_CC): zz=35 assert supply_AA_BB_CC[1]==zz,"bb and zz comparison failed" def test_comparewithCC(supply_AA_BB_CC): zz=35 assert supply_AA_BB_CC[2]==zz,"cc and zz comparison failed"
Тук:
- Имаме приспособление с име supply_AA_BB_CC. Този метод връща списък от три стойности.
- Имаме три метода за тестване, сравняващи всяка от стойностите.
Всяка от тестовите функции има входен аргумент, чието име съвпада с налично фиксиращо устройство (fixture). След това Pytest извиква съответния метод на фиксиращото устройство (fixture) и върнатите стойности се съхраняват във входния аргумент, тук списъкът [25,35,45]. Елементите от списъка след това се използват в тестовите методи за сравнение. Сега изпълнете теста и вижте резултата.
py.test test_basic_fixture
test_basic_fixture.py::test_comparewithAA FAILED
test_basic_fixture.py::test_comparewithBB PASSED
test_basic_fixture.py::test_comparewithCC FAILED
============================================== FAILURES ==============================================
_________________________________________ test_comparewithAA _________________________________________
supply_AA_BB_CC = [25, 35, 45]
def test_comparewithAA(supply_AA_BB_CC):
zz=35
> assert supply_AA_BB_CC[0]==zz,"aa and zz comparison failed"
E AssertionError: aa and zz comparison failed
E assert 25 == 35
test_basic_fixture.py:10: AssertionError
_________________________________________ test_comparewithCC _________________________________________
supply_AA_BB_CC = [25, 35, 45]
def test_comparewithCC(supply_AA_BB_CC):
zz=35
> assert supply_AA_BB_CC[2]==zz,"cc and zz comparison failed"
E AssertionError: cc and zz comparison failed
E assert 45 == 35
test_basic_fixture.py:16: AssertionError
================================= 2 failed, 1 passed in 0.05 seconds =================================
Тестът test_comparewithBB преминава, тъй като zz=BB=35, а останалите два теста се провалят.
Методът fixture има обхват само в рамките на тестовия файл, където е дефиниран. Ако се опитаме да достъпим fixture в друг тестов файл, ще получим грешка, гласища „fixture“. 'supply_AA_BB_CC' не е намерен за методите за изпитване в другите файлове.
За да използваме един и същ фиксиращ елемент срещу множество тестови файлове, създаваме методи за фиксиращи елементи във файл, наречен conftest.py. Нека видим това с примера за pytest по-долу. Създайте три файла, conftest.py, test_basic_fixture.py и test_basic_fixture2.py, със следния код.
conftest.py
import pytest @pytest.fixture def supply_AA_BB_CC(): aa=25 bb =35 cc=45 return [aa,bb,cc]
test_basic_fixture.py
import pytest def test_comparewithAA(supply_AA_BB_CC): zz=35 assert supply_AA_BB_CC[0]==zz,"aa and zz comparison failed" def test_comparewithBB(supply_AA_BB_CC): zz=35 assert supply_AA_BB_CC[1]==zz,"bb and zz comparison failed" def test_comparewithCC(supply_AA_BB_CC): zz=35 assert supply_AA_BB_CC[2]==zz,"cc and zz comparison failed"
test_basic_fixture2.py
import pytest def test_comparewithAA_file2(supply_AA_BB_CC): zz=25 assert supply_AA_BB_CC[0]==zz,"aa and zz comparison failed" def test_comparewithBB_file2(supply_AA_BB_CC): zz=25 assert supply_AA_BB_CC[1]==zz,"bb and zz comparison failed" def test_comparewithCC_file2(supply_AA_BB_CC): zz=25 assert supply_AA_BB_CC[2]==zz,"cc and zz comparison failed"
Pytest първо ще потърси фиксиращото устройство в тестовия файл и ако не бъде намерено, ще го направи в conftest.py. Стартирайте теста с py.test -k test_comparewith -v, за да получите резултата по-долу.
test_basic_fixture.py::test_comparewithAA FAILED test_basic_fixture.py::test_comparewithBB PASSED test_basic_fixture.py::test_comparewithCC FAILED test_basic_fixture2.py::test_comparewithAA_file2 PASSED test_basic_fixture2.py::test_comparewithBB_file2 FAILED test_basic_fixture2.py::test_comparewithCC_file2 FAILED
Pytest параметризиран тест
Целта на параметризирането на тест е да се проведе тест спрямо множество набори от аргументи. Можем да направим това с @pytest.mark.parametrize. Ще видим това в примера с pytest по-долу. Тук ще предадем три аргумента на тестов метод. Този тестов метод ще добави първите два аргумента и ще сравни резултата с третия аргумент. Създайте тестовия файл test_addition.py с кода по-долу.
import pytest @pytest.mark.parametrize("input1, input2, output",[(5,5,10),(3,5,12)]) def test_add(input1, input2, output): assert input1+input2 == output,"failed"
Тук тестовият метод приема три аргумента: input1, input2 и output. Той сумира input1 и input2 и сравнява сумата с output. Нека изпълним теста с py.test -k test_add -v и да видим резултата.
test_addition.py::test_add[5-5-10] PASSED
test_addition.py::test_add[3-5-12] FAILED
============================================== FAILURES ==============================================
__________________________________________ test_add[3-5-12] __________________________________________
input1 = 3, input2 = 5, output = 12
@pytest.mark.parametrize("input1, input2, output",[(5,5,10),(3,5,12)])
def test_add(input1, input2, output):
> assert input1+input2 == output,"failed"
E AssertionError: failed
E assert (3 + 5) == 12
test_addition.py:5: AssertionError
Можете да видите, че тестовете са изпълнени два пъти, единият проверява 5+5 ==10, а другият проверява 3+5 ==12.
test_addition.py::test_add[5-5-10] УДАРЕНО
test_addition.py::test_add[3-5-12] НЕУСПЕШНО
Pytest Xfail и Skip Tests
Ще има ситуации, в които не искаме да изпълним тест или тестов случай не е от значение в определен момент. В тези ситуации имаме възможност да xfail теста или да пропуснем тестовете. Тестът xfailed ще бъде изпълнен, но няма да бъде отчетен като част от неуспешните или издържаните тестове. Няма да има traceback се показва, ако тестът е неуспешен. Можем да направим xfail тестове, използвайки @pytest.mark.xfail. Пропусканеping „тест“ означава, че тестът няма да бъде изпълнен. Можем да пропуснем тестовете, използвайки @pytest.mark.skip. Редактирайте test_addition.py с кода по-долу.
import pytest @pytest.mark.skip def test_add_1(): assert 100+200 == 400,"failed" @pytest.mark.skip def test_add_2(): assert 100+200 == 300,"failed" @pytest.mark.xfail def test_add_3(): assert 15+13 == 28,"failed" @pytest.mark.xfail def test_add_4(): assert 15+13 == 100,"failed" def test_add_5(): assert 3+2 == 5,"failed" def test_add_6(): assert 3+2 == 6,"failed"
Тук:
- test_add_1 и test_add_2 се пропускат и няма да бъдат изпълнени.
- test_add_3 и test_add_4 са xfailed. Тези тестове ще бъдат изпълнени и ще бъдат част от тестовете xfailed (при неуспешен тест) или xpassed (при успешен тест). Няма да има никакви tracобратно изявление за неуспехи.
- test_add_5 и test_add_6 ще бъдат изпълнени, а test_add_6 ще докладва за неуспех с traceback, докато test_add_5 преминава.
Изпълнете теста с py.test test_addition.py -v и вижте резултата.
test_addition.py::test_add_1 SKIPPED
test_addition.py::test_add_2 SKIPPED
test_addition.py::test_add_3 XPASS
test_addition.py::test_add_4 xfail
test_addition.py::test_add_5 PASSED
test_addition.py::test_add_6 FAILED
============================================== FAILURES ==============================================
_____________________________________________ test_add_6 _____________________________________________
def test_add_6():
> assert 3+2 == 6,"failed"
E AssertionError: failed
E assert (3 + 2) == 6
test_addition.py:24: AssertionError
================ 1 failed, 1 passed, 2 skipped, 1 xfailed, 1 xpassed in 0.07 seconds =================
Резултати във XML формат
Можем да създаваме резултати от тестове в XML формат, които да подаваме към сървъри за непрекъсната интеграция за по-нататъшна обработка. Това може да се направи с командата py.test test_sample1.py -v –junitxml=”result.xml”. result.xml ще запише резултата от изпълнението на теста. По-долу можете да намерите примерен result.xml.
<?xml version="1.0" encoding="UTF-8"?> <testsuite errors="0" failures="1" name="pytest" skips="0" tests="2" time="0.046"> <testcase classname="test_sample1" file="test_sample1.py" line="3" name="test_file1_method1" time="0.001384973526"> <failure message="AssertionError:test failed because x=5 y=6 assert 5 ==6"> @pytest.mark.set1 def test_file1_method1(): x=5 y=6 assert x+1 == y,"test failed" > assert x == y,"test failed because x=" + str(x) + " y=" + str(y) E AssertionError: test failed because x=5 y=6 E assert 5 == 6 test_sample1.py:9: AssertionError </failure> </testcase> <testcase classname="test_sample1" file="test_sample1.py" line="10" name="test_file1_method2" time="0.000830173492432" /> </testsuite>
От Виждаме общо два теста, единият от които е неуспешен. По-долу можете да видите подробностите относно всеки изпълнен тест под етикет.
Pytest Framework за тестване на API
Сега ще създадем малка рамка pytest за тестване на API. Използваният тук API е безплатен от reqres.inТози уебсайт просто предоставя тестваем API и не съхранява нашите данни. Тук ще напишем някои тестове за следното:
- Изброяване на някои потребители.
- Вход с потребители.
Създайте файловете по-долу с дадения код. Първо, conftest.py има фикстура, която ще предостави базата URL за всички методи на изпитване.
import pytest @pytest.fixture def supply_url(): return "https://reqres.in/api"
След това, test_list_user.py съдържа методите за тестване за изброяване на валидни и невалидни потребители.
- test_list_valid_user проверява за валидно потребителско извличане и проверява отговора.
- test_list_invaliduser проверява за невалиден потребител и проверява отговора.
import pytest import requests import json @pytest.mark.parametrize("userid, firstname",[(1,"George"),(2,"Janet")]) def test_list_valid_user(supply_url,userid,firstname): url = supply_url + "/users/" + str(userid) resp = requests.get(url) j = json.loads(resp.text) assert resp.status_code == 200, resp.text assert j['data']['id'] == userid, resp.text assert j['data']['first_name'] == firstname, resp.text def test_list_invaliduser(supply_url): url = supply_url + "/users/50" resp = requests.get(url) assert resp.status_code == 404, resp.text
След това, test_login_user.py съдържа методите за тестване на функционалността за вход.
- test_login_valid проверява валидния опит за влизане с имейл и парола.
- test_login_no_password проверява невалидния опит за влизане без въвеждане на парола.
- test_login_no_email проверява невалидния опит за влизане без подаване на имейл.
import pytest import requests import json def test_login_valid(supply_url): url = supply_url + "/login/" data = {'email':'test@test.com','password':'something'} resp = requests.post(url, data=data) j = json.loads(resp.text) assert resp.status_code == 200, resp.text assert j['token'] == "QpwL5tke4Pnpja7X", resp.text def test_login_no_password(supply_url): url = supply_url + "/login/" data = {'email':'test@test.com'} resp = requests.post(url, data=data) j = json.loads(resp.text) assert resp.status_code == 400, resp.text assert j['error'] == "Missing password", resp.text def test_login_no_email(supply_url): url = supply_url + "/login/" data = {} resp = requests.post(url, data=data) j = json.loads(resp.text) assert resp.status_code == 400, resp.text assert j['error'] == "Missing email or username", resp.text
Изпълнете теста, използвайки py.test -v, и вижте резултата, както следва.
test_list_user.py::test_list_valid_user[1-George] PASSED test_list_user.py::test_list_valid_user[2-Janet] PASSED test_list_user.py::test_list_invaliduser PASSED test_login_user.py::test_login_valid PASSED test_login_user.py::test_login_no_password PASSED test_login_user.py::test_login_no_email PASSED
Актуализирайте тестовете и опитайте различни резултати.




