Kembali Selanjutnya 
Anda akan belajar bagaimana mengatur tes ke dalam kelas, modul, dan direktori. Saya kemudian akan menunjukkan kepada Anda bagaimana menggunakan marker untuk menandai tes mana yang ingin Anda jalankan, dan membahas bagaimana marker built-in dapat membantu Anda melewati tes dan menandai tes, mengharapkan kegagalan. Akhirnya, saya akan berbicara tentang parameterisasi tes, yang memungkinkan tes dipanggil dengan data yang berbeda.

Contoh-contoh dalam buku ini ditulis menggunakan Python 3.6 dan pytest 3.2. pytest 3.2 mendukung Python 2.6, 2.7, dan Python 3.3+.
Kode sumber untuk proyek Tugas, serta untuk semua tes yang ditunjukkan dalam buku ini, tersedia di tautan di halaman web buku di pragprog.com . Anda tidak perlu mengunduh kode sumber untuk memahami kode uji; kode uji disajikan dalam bentuk yang mudah dalam contoh. Tetapi untuk mengikuti tugas-tugas proyek, atau mengadaptasi contoh uji untuk menguji proyek Anda sendiri (tangan Anda tidak terikat!), Anda harus pergi ke halaman web buku dan mengunduh karya. Di sana, di halaman web buku, ada tautan untuk pesan errata dan forum diskusi .
Di bawah spoiler adalah daftar artikel dalam seri ini.
Di bab sebelumnya, Anda menjalankan pytest. Anda melihat cara menjalankannya dengan file dan direktori dan berapa banyak opsi yang berfungsi. Dalam bab ini, Anda akan belajar cara menulis fungsi pengujian dalam konteks pengujian paket Python. Jika Anda menggunakan pytest untuk menguji apa pun selain paket Python, sebagian besar bab ini akan sangat membantu.
Kami akan menulis tes untuk paket Tugas. Sebelum kita melakukan ini, saya akan berbicara tentang struktur paket distribusi Python dan tes untuk itu, serta bagaimana membuat tes melihat paket tes. Lalu saya akan menunjukkan kepada Anda bagaimana menggunakan assert dalam tes, bagaimana tes menangani pengecualian yang tidak terduga dan menguji pengecualian yang diharapkan.
Pada akhirnya, kami akan memiliki banyak tes. Dengan cara ini Anda akan belajar cara mengatur tes ke dalam kelas, modul, dan direktori. Saya kemudian akan menunjukkan kepada Anda bagaimana menggunakan marker untuk menandai tes mana yang ingin Anda jalankan, dan membahas bagaimana marker built-in dapat membantu Anda melewati tes dan menandai tes, mengharapkan kegagalan. Akhirnya, saya akan berbicara tentang parameterisasi tes, yang memungkinkan tes dipanggil dengan data yang berbeda.
Catatan Penerjemah: Jika Anda menggunakan Python 3.5 atau 3.6, maka ketika Anda menjalankan tes di Bab 2, Anda mungkin menerima pesan seperti ini

Masalah ini diobati dengan memperbaiki ...\code\tasks_proj\src\tasks\tasksdb_tinydb.py
dan menginstal ulang paket tugas
$ cd /path/to/code $ pip install ./tasks_proj/`
Anda eids
doc_ids
parameter bernama eids
pada doc_ids
dan doc_id
pada doc_id
dalam modul ...\code\tasks_proj\src\tasks\tasksdb_tinydb.py
Penjelasan Lihat #83783
sini.
Paket pengujian
Untuk mempelajari cara menulis fungsi tes untuk paket Python, kami akan menggunakan proyek Tugas sampel seperti yang dijelaskan dalam proyek Tugas di halaman xii. Tugas adalah paket Python yang mencakup alat baris perintah dengan nama tugas yang sama.
Apendiks 4, Pengemasan dan Distribusi Proyek Python di halaman 175, memberikan penjelasan tentang bagaimana mendistribusikan proyek Anda secara lokal dalam tim kecil atau global melalui PyPI, jadi saya tidak akan merinci bagaimana melakukan ini; namun, mari kita cepat melihat apa yang ada di proyek Tugas dan bagaimana file yang berbeda masuk ke dalam sejarah pengujian proyek ini.
Berikut ini adalah struktur file proyek Tugas:
tasks_proj/ βββ CHANGELOG.rst βββ LICENSE βββ MANIFEST.in βββ README.rst βββ setup.py βββ src β βββ tasks β βββ __init__.py β βββ api.py β βββ cli.py β βββ config.py β βββ tasksdb_pymongo.py β βββ tasksdb_tinydb.py βββ tests βββ conftest.py βββ pytest.ini βββ func β βββ __init__.py β βββ test_add.py β βββ ... βββ unit βββ __init__.py βββ test_task.py βββ ...
Saya menyertakan daftar lengkap proyek (dengan pengecualian daftar lengkap file tes) untuk menunjukkan bagaimana tes sesuai dengan sisa proyek dan menunjuk ke beberapa file yang merupakan kunci untuk pengujian, yaitu conftest.py, pytest.ini , berbagai __init__.py
file __init__.py
dan setup.py .
Semua tes disimpan dalam tes dan terpisah dari file sumber paket di src . Ini bukan persyaratan terbaik, tetapi ini adalah praktik terbaik.
Semua file tingkat atas, CHANGELOG.rst, LICENSE, README.rst, MANIFEST.in , dan setup.py , dibahas secara lebih rinci di Lampiran 4, Proyek Pengemasan dan Distribusi Python, di halaman 175. Meskipun setup.py penting untuk membangun distribusi dari paket, serta untuk kemampuan menginstal paket secara lokal sehingga paket tersedia untuk impor.
Tes fungsional dan unit dibagi ke dalam katalog mereka sendiri. Ini adalah keputusan yang sewenang-wenang dan tidak perlu. Namun, pengorganisasian file tes ke dalam beberapa direktori membuatnya mudah untuk menjalankan subset dari tes. Saya suka memisahkan tes fungsional dan unit, karena tes fungsional hanya akan pecah jika kami dengan sengaja mengubah fungsionalitas sistem, sementara tes unit dapat rusak selama perubahan refactoring atau implementasi.
Proyek ini berisi dua jenis file __init__.py
: yang ditemukan di direktori src/
dan yang ditemukan di tests/
. File src/tasks/__init__.py
memberi tahu Python bahwa direktori adalah sebuah paket. Itu juga bertindak sebagai antarmuka utama untuk paket ketika seseorang menggunakan import tasks
. Ini berisi kode untuk mengimpor fungsi tertentu dari api.py
, sehingga cli.py
dan file pengujian kami dapat mengakses fungsi paket, misalnya tasks.add()
, alih-alih menjalankan task.api.add ()
. File tests/func/__init__.py
dan tests/unit/__init__.py
kosong. Mereka memberi tahu pytest untuk naik satu direktori untuk menemukan root dari direktori tes dan file pytest.ini
.
File pytest.ini
adalah opsional. Ini berisi konfigurasi pytest umum untuk seluruh proyek. Proyek Anda seharusnya tidak lebih dari satu. Mungkin berisi arahan yang mengubah perilaku pytest, misalnya, menyiapkan daftar parameter yang akan selalu digunakan. Anda akan mempelajari semua tentang pytest.ini
di Bab 6, βKonfigurasi,β di halaman 113.
File conftest.py juga opsional. Itu dianggap pytest sebagai "plugin lokal" dan mungkin berisi fungsi dan perlengkapan kait. Fungsi kait adalah cara untuk menyematkan kode di bagian runtime pytest untuk mengubah cara kerja pytest. Fixture adalah fungsi pengaturan dan teardown yang berjalan sebelum dan sesudah fungsi tes dan dapat digunakan untuk mewakili sumber daya dan data yang digunakan oleh tes. (Fixture dibahas dalam Bab 3, Fixture pytest, pada halaman 49 dan Bab 4, Fixture Fixin, pada halaman 71, dan fungsi kait dibahas dalam Bab 5 "Plugin" pada halaman 95.) Fungsi dan fiting hook yang digunakan dalam tes di beberapa subdirektori harus dimuat dalam tes / conftest.py. Anda dapat memiliki beberapa file conftest.py; misalnya, Anda dapat memiliki satu di tes dan satu untuk setiap subdirektori tes.
Jika Anda belum melakukannya, Anda dapat mengunduh salinan kode sumber untuk proyek ini di situs web buku. Atau, Anda dapat mengerjakan proyek Anda dengan struktur yang sama.
Inilah test_task.py:
ch2 / task_proj / test / unit / test_task.py
"""Test the Task data type.""" # -*- coding: utf-8 -*- from tasks import Task def test_asdict(): """_asdict() .""" t_task = Task('do something', 'okken', True, 21) t_dict = t_task._asdict() expected = {'summary': 'do something', 'owner': 'okken', 'done': True, 'id': 21} assert t_dict == expected def test_replace(): """replace () .""" t_before = Task('finish book', 'brian', False) t_after = t_before._replace(id=10, done=True) t_expected = Task('finish book', 'brian', True, 10) assert t_after == t_expected def test_defaults(): """ .""" t1 = Task() t2 = Task(None, None, False, None) assert t1 == t2 def test_member_access(): """ .field namedtuple.""" t = Task('buy milk', 'brian') assert t.summary == 'buy milk' assert t.owner == 'brian' assert (t.done, t.id) == (False, None)
File test_task.py berisi pernyataan impor ini:
from tasks import Task
Cara terbaik untuk membiarkan tes mengimpor tugas atau mengimpor sesuatu dari tugas adalah menginstal tugas secara lokal menggunakan pip. Ini dimungkinkan karena ada file setup.py untuk memanggil pip secara langsung.
Instal tugas dengan menjalankan pip install .
atau pip install -e .
dari direktori task_proj. Atau opsi lain untuk menjalankan pip install -e tasks_proj
dari direktori satu tingkat lebih tinggi:
$ cd /path/to/code $ pip install ./tasks_proj/ $ pip install --no-cache-dir ./tasks_proj/ Processing ./tasks_proj Collecting click (from tasks==0.1.0) Downloading click-6.7-py2.py3-none-any.whl (71kB) ... Collecting tinydb (from tasks==0.1.0) Downloading tinydb-3.4.0.tar.gz Collecting six (from tasks==0.1.0) Downloading six-1.10.0-py2.py3-none-any.whl Installing collected packages: click, tinydb, six, tasks Running setup.py install for tinydb ... done Running setup.py install for tasks ... done Successfully installed click-6.7 six-1.10.0 tasks-0.1.0 tinydb-3.4.0
Jika Anda hanya ingin menjalankan tes untuk tugas, perintah ini akan dilakukan. Jika Anda ingin dapat mengubah kode sumber selama instalasi tugas, Anda perlu menggunakan instalasi dengan opsi -e (untuk "diedit" yang dapat diedit):
$ pip install -e ./tasks_proj/ Obtaining file:///path/to/code/tasks_proj Requirement already satisfied: click in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Requirement already satisfied: tinydb in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Requirement already satisfied: six in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Installing collected packages: tasks Found existing installation: tasks 0.1.0 Uninstalling tasks-0.1.0: Successfully uninstalled tasks-0.1.0 Running setup.py develop for tasks Successfully installed tasks
Sekarang coba jalankan tes:
$ cd /path/to/code/ch2/tasks_proj/tests/unit $ pytest test_task.py ===================== test session starts ====================== collected 4 items test_task.py .... =================== 4 passed in 0.01 seconds ===================
Impor telah berhasil! Tes lain sekarang dapat dengan aman menggunakan tugas impor. Sekarang mari kita menulis beberapa tes.
Menggunakan pernyataan tegas
Saat Anda menulis fungsi pengujian, pernyataan Python reguler adalah alat utama Anda untuk melaporkan kegagalan pengujian. Kesederhanaan ini di pytest sangat brilian. Inilah yang membuat banyak pengembang menggunakan pytest di atas kerangka kerja lainnya.
Jika Anda menggunakan platform pengujian lain, Anda mungkin melihat berbagai fungsi pembantu yang menegaskan. Misalnya, berikut ini adalah daftar beberapa bentuk fungsi pembantu dan penegasan:
Dengan pytest Anda dapat menggunakan menegaskan <ekspresi> dengan ekspresi apa pun. Jika ekspresi dievaluasi menjadi False ketika dikonversi ke bool, tes akan gagal.
pytest termasuk fungsi yang disebut penulisan ulang menegaskan yang memotong panggilan menegaskan dan menggantikannya dengan yang dapat memberi tahu Anda lebih banyak tentang mengapa laporan Anda gagal. Mari kita lihat betapa bermanfaat penulisan ulang ini jika kita melihat beberapa kesalahan pernyataan:
ch2 / task_proj / test / unit / test_task_fail.py
""" the Task type .""" from tasks import Task def test_task_equality(): """ .""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') assert t1 == t2 def test_dict_equality(): """ , dicts, .""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() assert t1_dict == t2_dict
Semua tes ini gagal, tetapi informasi dalam penelusuran menarik:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\unit>pytest test_task_fail.py ============================= test session starts ============================= collected 2 items test_task_fail.py FF ================================== FAILURES =================================== _____________________________ test_task_equality ______________________________ def test_task_equality(): """Different tasks should not be equal.""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') > assert t1 == t2 E AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None) E At index 0 diff: 'sit there' != 'do something' E Use -v to get the full diff test_task_fail.py:9: AssertionError _____________________________ test_dict_equality ______________________________ def test_dict_equality(): """Different tasks compared as dicts should not be equal.""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() > assert t1_dict == t2_dict E AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)]) E Omitting 3 identical items, use -vv to show E Differing items: E {'owner': 'okken'} != {'owner': 'okkem'} E Use -v to get the full diff test_task_fail.py:16: AssertionError ========================== 2 failed in 0.30 seconds ===========================
Wow! Ini banyak informasi. Untuk setiap tes yang gagal, string kesalahan yang tepat ditampilkan dengan> pointer kegagalan. Baris E menunjukkan informasi tambahan tentang kegagalan yang tegas untuk membantu Anda memahami apa yang salah.
Saya sengaja memasukkan dua ketidakcocokan dalam test_task_equality()
, tetapi hanya yang pertama yang ditunjukkan dalam kode sebelumnya. Mari kita coba lagi dengan flag -v
, seperti yang disarankan dalam pesan kesalahan:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\unit>pytest -v test_task_fail.py ============================= test session starts ============================= collected 2 items test_task_fail.py::test_task_equality FAILED test_task_fail.py::test_dict_equality FAILED ================================== FAILURES =================================== _____________________________ test_task_equality ______________________________ def test_task_equality(): """Different tasks should not be equal.""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') > assert t1 == t2 E AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None) E At index 0 diff: 'sit there' != 'do something' E Full diff: E - Task(summary='sit there', owner='brian', done=False, id=None) E ? ^^^ ^^^ ^^^^ E + Task(summary='do something', owner='okken', done=False, id=None) E ? +++ ^^^ ^^^ ^^^^ test_task_fail.py:9: AssertionError _____________________________ test_dict_equality ______________________________ def test_dict_equality(): """Different tasks compared as dicts should not be equal.""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() > assert t1_dict == t2_dict E AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)]) E Omitting 3 identical items, use -vv to show E Differing items: E {'owner': 'okken'} != {'owner': 'okkem'} E Full diff: E {'summary': 'make sandwich', E - 'owner': 'okken', E ? ^... E E ...Full output truncated (5 lines hidden), use '-vv' to show test_task_fail.py:16: AssertionError ========================== 2 failed in 0.28 seconds ===========================
Yah, kurasa itu sangat keren! pytest tidak hanya dapat menemukan kedua perbedaan, tetapi juga menunjukkan kepada kita di mana perbedaan-perbedaan ini berada. Contoh ini hanya menggunakan pernyataan kesetaraan; Anda dapat menemukan lebih banyak variasi pernyataan pernyataan di pytest.org dengan informasi debugging jejak yang menakjubkan.
Pengecualian yang diharapkan
Pengecualian dapat terjadi di beberapa tempat di API Tugas. Mari kita lihat fungsi-fungsi yang ditemukan di task / api.py :
def add(task): # type: (Task) -\> int def get(task_id): # type: (int) -\> Task def list_tasks(owner=None): # type: (str|None) -\> list of Task def count(): # type: (None) -\> int def update(task_id, task): # type: (int, Task) -\> None def delete(task_id): # type: (int) -\> None def delete_all(): # type: () -\> None def unique_id(): # type: () -\> int def start_tasks_db(db_path, db_type): # type: (str, str) -\> None def stop_tasks_db(): # type: () -\> None
Ada kesepakatan antara kode CLI di cli.py dan kode API di api.py mengenai jenis apa yang akan diteruskan ke fungsi API. Panggilan API adalah tempat saya perkirakan pengecualian akan dimunculkan jika jenisnya salah. Untuk memastikan bahwa fungsi-fungsi ini membuang pengecualian jika mereka tidak dipanggil dengan benar, gunakan tipe yang salah dalam fungsi tes untuk secara sengaja membuang pengecualian TypeError dan gunakan dengan pytest.raises (pengecualian yang diharapkan), misalnya:
ch2 / task_proj / tests / func / test_api_exceptions.py
""" - API.""" import pytest import tasks def test_add_raises(): """add() param.""" with pytest.raises(TypeError): tasks.add(task='not a Task object')
Di test_add_raises()
, dengan pytest.raises(TypeError)
: pernyataan melaporkan bahwa segala sesuatu di blok kode berikutnya harus membuang pengecualian TypeError. Jika pengecualian tidak dinaikkan, tes gagal. Jika tes menimbulkan pengecualian lain, itu gagal.
Kami baru saja memeriksa jenis pengecualian di test_add_raises()
. Anda juga dapat memeriksa opsi pengecualian. Untuk start_tasks_db(db_path, db_type)
, bukan hanya db_type yang harus berupa string, itu harus berupa 'mungil' atau 'mongo'. Anda dapat memeriksa untuk memastikan pesan pengecualian sudah benar dengan menambahkan excinfo:
ch2 / task_proj / tests / func / test_api_exceptions.py
def test_start_tasks_db_raises(): """, .""" with pytest.raises(ValueError) as excinfo: tasks.start_tasks_db('some/great/path', 'mysql') exception_msg = excinfo.value.args[0] assert exception_msg == "db_type must be a 'tiny' or 'mongo'"
Ini memungkinkan kita untuk melihat lebih dekat pada pengecualian ini. Nama variabel after as (dalam hal ini, excinfo) diisi dengan informasi pengecualian dan bertipe ExceptionInfo.
Dalam kasus kami, kami ingin memastikan bahwa parameter pengecualian pertama (dan hanya) cocok dengan string.
Menandai fungsi tes
pytest menyediakan mekanisme keren untuk menempatkan marker pada fungsi pengujian. Suatu tes dapat memiliki lebih dari satu marker, dan sebuah marker dapat dalam beberapa tes.
Penanda akan masuk akal bagi Anda setelah Anda melihat mereka beraksi. Misalkan kita ingin menjalankan subset dari tes kita sebagai "tes asap" cepat untuk mendapatkan gambaran apakah ada celah serius dalam sistem. Secara konvensional, tes Smoke tidak komprehensif, suite tes menyeluruh, tetapi subset terpilih yang dapat Anda jalankan dengan cepat dan memberikan pengembang gambar yang layak tentang kesehatan semua bagian sistem.
Untuk menambahkan suite tes asap ke proyek Tugas Anda, Anda perlu menambahkan @mark.pytest.smoke
untuk beberapa tes. Mari kita tambahkan ke beberapa test_api_exceptions.py
tes (perhatikan bahwa asap dan spidol tidak dibangun ke pytest; saya baru saja datang dengan mereka):
ch2 / task_proj / tests / func / test_api_exceptions.py
@pytest.mark.smoke def test_list_raises(): """list() param.""" with pytest.raises(TypeError): tasks.list_tasks(owner=123) @pytest.mark.get @pytest.mark.smoke def test_get_raises(): """get() param.""" with pytest.raises(TypeError): tasks.get(task_id='123')
Sekarang mari kita jalankan hanya tes-tes yang ditandai dengan -m marker_name
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests>cd func (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_list_raises PASSED test_api_exceptions.py::test_get_raises PASSED ============================= 5 tests deselected ============================== =================== 2 passed, 5 deselected in 0.18 seconds ==================== (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func> (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_get_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ====================
Ingat bahwa -v
kependekan dari --verbose
dan memungkinkan kita untuk melihat nama-nama tes yang sedang berjalan. Menggunakan -m 'smoke' menjalankan kedua pengujian, berlabel @ pytest.mark.smoke.
Menggunakan -m
'get' akan menjalankan satu tes bertanda @pytest.mark.get
. Cukup sederhana.
Semuanya menjadi mukjizat dan mukjizat! Ekspresi setelah -m
dapat menggunakan and
, or
dan not
menggabungkan beberapa penanda:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke and get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_get_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ====================
Kami melakukan tes ini hanya dengan smoke
dan get
spidol. Kita tidak bisa menggunakan:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke and not get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_list_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ====================
Menambahkan -m 'smoke and not get'
dipilih tes yang ditandai dengan @pytest.mark.smoke
tetapi tidak @pytest.mark.get
.
Isi Tes Asap
Tes sebelumnya belum tampak seperti smoke test
masuk akal. Kami sebenarnya tidak menyentuh database dan tidak menambahkan tugas apa pun. Tentu saja smoke test
harus melakukan ini.
Mari kita tambahkan beberapa tes yang mempertimbangkan untuk menambah tugas, dan gunakan salah satunya sebagai bagian dari rangkaian tes asap kami:
ch2 / task_proj / tests / func / test_add.py
""" API tasks.add ().""" import pytest import tasks from tasks import Task def test_add_returns_valid_id(): """tasks.add(valid task) .""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) assert isinstance(task_id, int) @pytest.mark.smoke def test_added_task_has_id_set(): """, task_id tasks.add().""" # GIVEN an initialized tasks db # AND a new task is added new_task = Task('sit in chair', owner='me', done=True) task_id = tasks.add(new_task) # WHEN task is retrieved task_from_db = tasks.get(task_id) # THEN task_id matches id field assert task_from_db.id == task_id
Kedua tes ini memiliki komentar GIVEN pada database tugas yang diinisialisasi, tetapi tidak ada database yang diinisialisasi dalam tes. Kita dapat mendefinisikan fixture untuk menginisialisasi database sebelum tes dan membersihkan setelah tes:
ch2 / task_proj / tests / func / test_add.py
@pytest.fixture(autouse=True) def initialized_tasks_db(tmpdir): """Connect to db before testing, disconnect after.""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield # # Teardown : stop db tasks.stop_tasks_db()
Fixture, tmpdir yang digunakan dalam contoh ini, adalah fixture bawaan. Anda akan mempelajari semua tentang perlengkapan bawaan di Bab 4, Perlengkapan Terpasang, di halaman 71, dan Anda akan belajar cara menulis perlengkapan Anda sendiri dan cara kerjanya di Bab 3, Perlengkapan terbaik, di halaman 49, termasuk parameter pemadaman yang digunakan di sini.
Autouse yang digunakan dalam pengujian kami menunjukkan bahwa semua tes dalam file ini akan menggunakan fixture. Kode sebelum yield
dijalankan sebelum setiap tes; kode setelah yield
dieksekusi setelah pengujian. Jika diinginkan, hasil dapat mengembalikan data ke tes. Anda akan mempertimbangkan semua ini dan banyak lagi di bab-bab berikut, tetapi di sini kita perlu mengkonfigurasi database untuk pengujian, jadi saya tidak bisa lagi menunggu dan harus menunjukkan perangkat ini kepada Anda (fixture tentunya!). (pytest juga mendukung pengaturan kuno dan fungsi teardown, seperti yang digunakan di unittest dan hidung , tetapi mereka tidak begitu menarik. Namun, jika Anda tertarik, mereka dijelaskan dalam Lampiran 5, xUnit Fixtures, di halaman 183.)
Mari kita tunda pembahasan jadwal pertandingan untuk saat ini dan pergi ke awal proyek dan menjalankan uji asap kami:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>cd .. (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests>cd .. (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -m "smoke" ============================= test session starts ============================= collected 56 items tests/func/test_add.py::test_added_task_has_id_set PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED ============================= 53 tests deselected ============================= =================== 3 passed, 53 deselected in 0.49 seconds ===================
Ini menunjukkan bahwa tes yang ditandai dari file yang berbeda dapat berjalan bersama.
Melewati Tes
Meskipun penanda yang dibahas dalam menandai metode verifikasi pada halaman 31 adalah nama-nama pilihan Anda, pytest menyertakan beberapa penanda skipif
: skip
, skipif
, dan xfail
. Di bagian ini saya akan berbicara tentang skip
dan skipif
, dan di -xfail
berikutnya.
skipif
skip
dan skipif
memungkinkan Anda untuk melewati tes yang tidak perlu dilakukan. Misalnya, katakanlah kita tidak tahu bagaimana tasks.unique_id()
harus berfungsi. Setiap panggilan harus mengembalikan nomor yang berbeda? , ?
-, (, initialized_tasks_db
; ):
ch2/tasks_proj/tests/func/ test_unique_id_1.py
"""Test tasks.unique_id().""" import pytest import tasks def test_unique_id(): """ unique_id () .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_1.py ============================= test session starts ============================= collected 1 item test_unique_id_1.py F ================================== FAILURES =================================== _______________________________ test_unique_id ________________________________ def test_unique_id(): """Calling unique_id() twice should return different numbers.""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() > assert id_1 != id_2 E assert 1 != 1 test_unique_id_1.py:11: AssertionError ========================== 1 failed in 0.30 seconds ===========================
Hm , . API , , docstring """Return an integer that does not exist in the db.""", , DB . . , :
ch2/tasks_proj/tests/func/ test_unique_id_2.py
@pytest.mark.skip(reason='misunderstood the API') def test_unique_id_1(): """ unique_id () .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 def test_unique_id_2(): """unique_id() id.""" ids = [] ids.append(tasks.add(Task('one'))) ids.append(tasks.add(Task('two'))) ids.append(tasks.add(Task('three'))) # id uid = tasks.unique_id() # , assert uid not in ids
, , , @pytest..skip()
.
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_2.py ============================= test session starts ============================= collected 2 items test_unique_id_2.py::test_unique_id_1 SKIPPED test_unique_id_2.py::test_unique_id_2 PASSED ===================== 1 passed, 1 skipped in 0.19 seconds =====================
, - , , 0.2.0 . skipif:
ch2/tasks_proj/tests/func/ test_unique_id_3.py
@pytest.mark.skipif(tasks.__version__ < '0.2.0', reason='not supported until version 0.2.0') def test_unique_id_1(): """ unique_id () .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2
, skipif()
, Python. , , . skip , skipif . skip , skipif . ( reason ) skip , skipif xfail . :
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py s. ===================== 1 passed, 1 skipped in 0.20 seconds =====================
s.
, (skipped), (passed). , - -v
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py::test_unique_id_1 SKIPPED test_unique_id_3.py::test_unique_id_2 PASSED ===================== 1 passed, 1 skipped in 0.19 seconds =====================
. -rs
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -rs test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py s. =========================== short test summary info =========================== SKIP [1] func\test_unique_id_3.py:8: not supported until version 0.2.0 ===================== 1 passed, 1 skipped in 0.22 seconds =====================
-r chars
:
$ pytest --help ... -r chars show extra test summary info as specified by chars ( , ) (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed, (p)passed, (P)passed with output, (a)all except pP. ...
, .
skip
skipif
, . xfail
pytest , , . unique_id ()
, xfail
:
ch2/tasks_proj/tests/func/ test_unique_id_4.py
@pytest.mark.xfail(tasks.__version__ < '0.2.0', reason='not supported until version 0.2.0') def test_unique_id_1(): """ unique_id() .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 @pytest.mark.xfail() def test_unique_id_is_a_duck(): """ xfail.""" uid = tasks.unique_id() assert uid == 'a duck' @pytest.mark.xfail() def test_unique_id_not_a_duck(): """ xpass.""" uid = tasks.unique_id() assert uid != 'a duck'
Running this shows:
, , xfail
. == vs.! =. .
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_4.py ============================= test session starts ============================= collected 4 items test_unique_id_4.py xxX. =============== 1 passed, 2 xfailed, 1 xpassed in 0.36 seconds ================
X XFAIL, Β« ( expected to fail )Β». X XPASS Β«, , ( expected to fail but passed. )Β».
--verbose
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_4.py ============================= test session starts ============================= collected 4 items test_unique_id_4.py::test_unique_id_1 xfail test_unique_id_4.py::test_unique_id_is_a_duck xfail test_unique_id_4.py::test_unique_id_not_a_duck XPASS test_unique_id_4.py::test_unique_id_2 PASSED =============== 1 passed, 2 xfailed, 1 xpassed in 0.36 seconds ================
pytest , , , xfail
, FAIL. pytest.ini :
[pytest] xfail_strict=true
pytest.ini 6, , . 113.
, ββ . . , , . , . . .
A Single Directory
, pytest :
(venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest tests\func --tb=no ============================= test session starts ============================= collected 50 items tests\func\test_add.py .. tests\func\test_add_variety.py ................................ tests\func\test_api_exceptions.py ....... tests\func\test_unique_id_1.py F tests\func\test_unique_id_2.py s. tests\func\test_unique_id_3.py s. tests\func\test_unique_id_4.py xxX. ==== 1 failed, 44 passed, 2 skipped, 2 xfailed, 1 xpassed in 1.75 seconds =====
, -v
, .
(venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v tests\func --tb=no ============================= test session starts =============================
...
collected 50 items tests\func\test_add.py::test_add_returns_valid_id PASSED tests\func\test_add.py::test_added_task_has_id_set PASSED tests\func\test_add_variety.py::test_add_1 PASSED tests\func\test_add_variety.py::test_add_2[task0] PASSED tests\func\test_add_variety.py::test_add_2[task1] PASSED tests\func\test_add_variety.py::test_add_2[task2] PASSED tests\func\test_add_variety.py::test_add_2[task3] PASSED tests\func\test_add_variety.py::test_add_3[sleep-None-False] PASSED ... tests\func\test_unique_id_2.py::test_unique_id_1 SKIPPED tests\func\test_unique_id_2.py::test_unique_id_2 PASSED ... tests\func\test_unique_id_4.py::test_unique_id_1 xfail tests\func\test_unique_id_4.py::test_unique_id_is_a_duck xfail tests\func\test_unique_id_4.py::test_unique_id_not_a_duck XPASS tests\func\test_unique_id_4.py::test_unique_id_2 PASSED ==== 1 failed, 44 passed, 2 skipped, 2 xfailed, 1 xpassed in 2.05 seconds =====
, .
File/Module
, , pytest:
$ cd /path/to/code/ch2/tasks_proj $ pytest tests/func/test_add.py =========================== test session starts =========================== collected 2 items tests/func/test_add.py .. ======================== 2 passed in 0.05 seconds =========================
.
, ::
:
$ cd /path/to/code/ch2/tasks_proj $ pytest -v tests/func/test_add.py::test_add_returns_valid_id =========================== test session starts =========================== collected 3 items tests/func/test_add.py::test_add_returns_valid_id PASSED ======================== 1 passed in 0.02 seconds =========================
-v
, , .
Test Class
Here's an example:
β , .
Berikut ini sebuah contoh:
ch2/tasks_proj/tests/func/ test_api_exceptions.py
class TestUpdate(): """ tasks.update().""" def test_bad_id(self): """non-int id excption.""" with pytest.raises(TypeError): tasks.update(task_id={'dict instead': 1}, task=tasks.Task()) def test_bad_task(self): """A non-Task task excption.""" with pytest.raises(TypeError): tasks.update(task_id=1, task='not a task')
, update()
, . , , ::
, :
(venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v tests/func/test_api_exceptions.py::TestUpdate ============================= test session starts ============================= collected 2 items tests\func\test_api_exceptions.py::TestUpdate::test_bad_id PASSED tests\func\test_api_exceptions.py::TestUpdate::test_bad_task PASSED ========================== 2 passed in 0.12 seconds ===========================
A Single Test Method of a Test Class
, β ::
:
$ cd /path/to/code/ch2/tasks_proj $ pytest -v tests/func/test_api_exceptions.py::TestUpdate::test_bad_id ===================== test session starts ====================== collected 1 item tests/func/test_api_exceptions.py::TestUpdate::test_bad_id PASSED =================== 1 passed in 0.03 seconds ===================
,
, , , , . , pytest -v
.
-k
, . and
, or
not
. , _raises
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -k _raises ============================= test session starts ============================= collected 56 items tests/func/test_api_exceptions.py::test_add_raises PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED tests/func/test_api_exceptions.py::test_delete_raises PASSED tests/func/test_api_exceptions.py::test_start_tasks_db_raises PASSED ============================= 51 tests deselected ============================= =================== 5 passed, 51 deselected in 0.54 seconds ===================
and
not
test_delete_raises()
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -k "_raises and not delete" ============================= test session starts ============================= collected 56 items tests/func/test_api_exceptions.py::test_add_raises PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED tests/func/test_api_exceptions.py::test_start_tasks_db_raises PASSED ============================= 52 tests deselected ============================= =================== 4 passed, 52 deselected in 0.44 seconds ===================
, , , -k
. , , .
[Parametrized Testing]:
, , . . - pytest, - .
, , add()
:
ch2/tasks_proj/tests/func/ test_add_variety.py
""" API tasks.add().""" import pytest import tasks from tasks import Task def test_add_1(): """tasks.get () id, add() works.""" task = Task('breathe', 'BRIAN', True) task_id = tasks.add(task) t_from_db = tasks.get(task_id) # , , assert equivalent(t_from_db, task) def equivalent(t1, t2): """ .""" # , id return ((t1.summary == t2.summary) and (t1.owner == t2.owner) and (t1.done == t2.done)) @pytest.fixture(autouse=True) def initialized_tasks_db(tmpdir): """ , .""" tasks.start_tasks_db(str(tmpdir), 'tiny') yield tasks.stop_tasks_db()
tasks id
None
. id
. ==
, , . equivalent()
, id
. autouse
, , . , :
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_1 ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_1 PASSED ========================== 1 passed in 0.69 seconds ===========================
. , . , ? . @pytest.mark.parametrize(argnames, argvalues)
, :
ch2/tasks_proj/tests/func/ test_add_variety.py
@pytest.mark.parametrize('task', [Task('sleep', done=True), Task('wake', 'brian'), Task('breathe', 'BRIAN', True), Task('exercise', 'BrIaN', False)]) def test_add_2(task): """ .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
parametrize()
β β 'task', . β , Task. pytest :
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_2 ============================= test session starts ============================= collected 4 items test_add_variety.py::test_add_2[task0] PASSED test_add_variety.py::test_add_2[task1] PASSED test_add_variety.py::test_add_2[task2] PASSED test_add_variety.py::test_add_2[task3] PASSED ========================== 4 passed in 0.69 seconds ===========================
parametrize()
. , , :
ch2/tasks_proj/tests/func/ test_add_variety.py
@pytest.mark.parametrize('summary, owner, done', [('sleep', None, False), ('wake', 'brian', False), ('breathe', 'BRIAN', True), ('eat eggs', 'BrIaN', False), ]) def test_add_3(summary, owner, done): """ .""" task = Task(summary, owner, done) task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
, pytest, , :
(venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_3 ============================= test session starts ============================= platform win32 -- Python 3.5.2, pytest-3.5.1, py-1.5.3, pluggy-0.6.0 -- cachedir: ..\.pytest_cache rootdir: ...\bopytest-code\code\ch2\tasks_proj\tests, inifile: pytest.ini collected 4 items test_add_variety.py::test_add_3[sleep-None-False] PASSED [ 25%] test_add_variety.py::test_add_3[wake-brian-False] PASSED [ 50%] test_add_variety.py::test_add_3[breathe-BRIAN-True] PASSED [ 75%] test_add_variety.py::test_add_3[eat eggs-BrIaN-False] PASSED [100%] ========================== 4 passed in 0.37 seconds ===========================
, , pytest, :
(venv35) c:\BOOK\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_3[sleep-None-False] ============================= test session starts ============================= test_add_variety.py::test_add_3[sleep-None-False] PASSED [100%] ========================== 1 passed in 0.22 seconds ===========================
, :
(venv35) c:\BOOK\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v "test_add_variety.py::test_add_3[eat eggs-BrIaN-False]" ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_3[eat eggs-BrIaN-False] PASSED [100%] ========================== 1 passed in 0.56 seconds ===========================
, :
ch2/tasks_proj/tests/func/ test_add_variety.py
tasks_to_try = (Task('sleep', done=True), Task('wake', 'brian'), Task('wake', 'brian'), Task('breathe', 'BRIAN', True), Task('exercise', 'BrIaN', False)) @pytest.mark.parametrize('task', tasks_to_try) def test_add_4(task): """ .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
. :
(venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_4 ============================= test session starts ============================= collected 5 items test_add_variety.py::test_add_4[task0] PASSED [ 20%] test_add_variety.py::test_add_4[task1] PASSED [ 40%] test_add_variety.py::test_add_4[task2] PASSED [ 60%] test_add_variety.py::test_add_4[task3] PASSED [ 80%] test_add_variety.py::test_add_4[task4] PASSED [100%] ========================== 5 passed in 0.34 seconds ===========================
, . , ids parametrize()
, . ids
, . , tasks_to_try
, :
ch2/tasks_proj/tests/func/ test_add_variety.py
task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) for t in tasks_to_try] @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) def test_add_5(task): """Demonstrate ids.""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
, :
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_5 ============================= test session starts ============================= collected 5 items test_add_variety.py::test_add_5[Task(sleep,None,True)] PASSED test_add_variety.py::test_add_5[Task(wake,brian,False)0] PASSED test_add_variety.py::test_add_5[Task(wake,brian,False)1] PASSED test_add_variety.py::test_add_5[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)] PASSED ========================== 5 passed in 0.45 seconds ===========================
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v "test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)]" ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)] PASSED ========================== 1 passed in 0.21 seconds ===========================
; shell. parametrize()
. :
ch2/tasks_proj/tests/func/ test_add_variety.py
@pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) class TestAdd(): """ .""" def test_equivalent(self, task): """ , .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task) def test_valid_id(self, task): """ .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert t_from_db.id == task_id
:
(venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::TestAdd ============================= test session starts ============================= collected 10 items test_add_variety.py::TestAdd::test_equivalent[Task(sleep,None,True)] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(wake,brian,False)0] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(wake,brian,False)1] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(exercise,BrIaN,False)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(sleep,None,True)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(wake,brian,False)0] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(wake,brian,False)1] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(exercise,BrIaN,False)] PASSED ========================== 10 passed in 1.16 seconds ==========================
, @pytest.mark.parametrize()
. pytest.param(<value\>, id="something")
:
:
(venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func $ pytest -v test_add_variety.py::test_add_6 ======================================== test session starts ========================================= collected 3 items test_add_variety.py::test_add_6[just summary] PASSED [ 33%] test_add_variety.py::test_add_6[summary\owner] PASSED [ 66%] test_add_variety.py::test_add_6[summary\owner\done] PASSED [100%] ================================ 3 passed, 6 warnings in 0.35 seconds ================================
, id
.
Latihan
- ,
task_proj
, - , pip install /path/to/tasks_proj
. - .
- pytest .
- pytest ,
tasks_proj/tests/func
. pytest , . . , ? - xfail , pytest tests .
tasks.count()
, . API , , , .- ?
test_api_exceptions.py
. , . ( api.py
.)
Apa selanjutnya
pytest . , , , . initialized_tasks_db
. / .
Mereka juga dapat memisahkan kode umum sehingga beberapa fungsi pengujian dapat menggunakan pengaturan yang sama. Dalam bab berikutnya, Anda akan terjun jauh ke dunia pytest yang indah.
Kembali Selanjutnya 