Kembali Selanjutnya 
Perlengkapan bawaan yang disertakan dengan pytest dapat membantu Anda melakukan beberapa hal yang cukup berguna dalam pengujian Anda dengan mudah dan alami. Sebagai contoh, selain menangani file sementara, pytest termasuk perlengkapan bawaan untuk mengakses parameter baris perintah, komunikasi antara sesi tes, memeriksa aliran output, mengubah variabel lingkungan, dan peringatan polling.

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 memeriksa proyek Anda sendiri (tangan Anda tidak terikat!), Anda harus membuka halaman web buku untuk mengunduh karya. Di sana, di halaman web buku, ada tautan ke pos errata dan forum diskusi .
Di bawah spoiler adalah daftar artikel dalam seri ini.
Dalam bab sebelumnya, Anda memeriksa perlengkapan apa, cara menulisnya, dan bagaimana menggunakannya untuk data uji, serta untuk pengaturan dan kode teardown.
Anda juga menggunakan conftest.py untuk berbagi perlengkapan antara pengujian di beberapa file pengujian. Pada akhir bab 3, perlengkapan berikut dipasang di Fixture pytest di halaman 49 dari proyek Tugas: tasks_db_session
, tasks_just_a_few
, tasks_mult_per_owner
, tasks_db
, db_with_3_tasks
dan db_with_multi_per_owner
ditentukan oleh fungsi apa pun yang dapat digunakan untuk menguji fungsi apa pun yang dapat digunakan untuk menguji fungsi yang digunakan oleh conft. membutuhkan mereka.
Menggunakan kembali perlengkapan reguler adalah ide yang bagus sehingga pengembang pytest menyertakan beberapa perlengkapan yang biasanya diperlukan di pytest. Anda telah melihat bagaimana tmpdir dan tmpdir_factory digunakan oleh proyek Tugas di bagian perubahan ruang lingkup untuk perlengkapan proyek Tugas di halaman 59. Anda akan membahasnya secara lebih rinci dalam bab ini.
Perlengkapan bawaan yang disertakan dengan pytest dapat membantu Anda melakukan beberapa hal yang cukup berguna dalam pengujian Anda dengan mudah dan alami. Sebagai contoh, selain menangani file sementara, pytest termasuk perlengkapan bawaan untuk mengakses parameter baris perintah, komunikasi antara sesi tes, memeriksa aliran output, mengubah variabel lingkungan, dan peringatan polling. Perlengkapan bawaan adalah ekstensi untuk fungsionalitas inti pytest. Mari sekarang kita lihat beberapa perlengkapan inline yang paling umum digunakan secara berurutan.
Menggunakan tmpdir dan tmpdir_factory
Jika Anda menguji sesuatu yang membaca, menulis, atau memodifikasi file, Anda dapat menggunakan tmpdir untuk membuat file atau direktori yang digunakan oleh satu pengujian, dan Anda dapat menggunakan tmpdir_factory
ketika Anda ingin mengatur direktori untuk beberapa tes.
tmpdir
tmpdir memiliki cakupan fungsi, dan fixture tmpdir_factory
memiliki cakupan sesi. Setiap tes tunggal yang memerlukan direktori atau file sementara hanya untuk satu tes dapat menggunakan tmpdir
. Ini juga berlaku untuk fixture, yang mengkustomisasi direktori atau file yang harus dibuat ulang untuk setiap fungsi tes.
Berikut adalah contoh sederhana menggunakan tmpdir
:
ch4/test_tmpdir.py
def test_tmpdir(tmpdir):
Nilai yang dikembalikan dari tmpdir
adalah objek bertipe py.path.local.1
yang tampaknya menjadi semua yang kita butuhkan untuk direktori dan file sementara. Namun, ada satu trik. Karena fixture tmpdir
didefinisikan sebagai lingkup fungsi , tmpdir
tidak dapat digunakan untuk membuat folder atau file yang harus tersedia lebih dari satu fungsi tes. Untuk perlengkapan dengan ruang lingkup selain fungsi (kelas, modul, sesi), tmpdir_factory
tersedia.
tmpdir_factory
tmpdir_factory sangat mirip dengan tmpdir
, tetapi memiliki antarmuka yang berbeda. Seperti dijelaskan di bagian “Spesifikasi Ruang Lingkup”, di halaman 56, perlengkapan area fungsi berjalan satu kali untuk setiap fungsi tes, perlengkapan wilayah modul dijalankan satu kali per modul, perlengkapan kelas satu kali untuk setiap kelas, dan tes validasi area bekerja sekali per sesi. Dengan demikian, sumber daya yang dibuat dalam catatan area sesi memiliki masa pakai seumur hidup seluruh sesi. Untuk menunjukkan betapa miripnya tmpdir
dan tmpdir_factory
, saya akan tmpdir
contoh tmpdir
, di mana tmpdir_factory
:
ch4 / test_tmpdir.py
def test_tmpdir_factory(tmpdir_factory):
Baris pertama menggunakan mktemp('mydir')
untuk membuat direktori dan menyimpannya di a_dir
. Untuk fungsi lainnya, Anda dapat menggunakan a_dir
dengan cara yang sama seperti tmpdir
dikembalikan dari tmpdir
.
Pada baris kedua dari contoh tmpdir_factory
, fungsi getbasetemp()
mengembalikan direktori dasar yang digunakan untuk sesi ini. Pernyataan cetak dalam contoh diperlukan agar Anda dapat melihat direktori pada sistem Anda. Mari kita lihat di mana tempatnya:
$ cd /path/to/code/ch4 $ pytest -q -s test_tmpdir.py::test_tmpdir_factory base: /private/var/folders/53/zv4j_zc506x2xq25l31qxvxm0000gn/T/pytest-of-okken/pytest-732 . 1 passed in 0.04 seconds
Direktori dasar ini bergantung pada sistem dan pengguna, dan pytest - NUM
berubah untuk setiap sesi dengan meningkatnya NUM
. Direktori dasar dibiarkan sendiri setelah sesi. pytest membersihkannya, dan hanya beberapa direktori basis sementara terbaru yang tersisa pada sistem, yang baik-baik saja jika Anda tidak sabar untuk memeriksa file setelah uji coba.
Anda juga dapat menentukan direktori basis Anda sendiri jika perlu dengan pytest --basetemp=mydir
.
Menggunakan direktori sementara untuk area lain
Kami mendapatkan direktori sementara dan file area sesi dari tmpdir_factory
, dan fungsi direktori dan file region dari tmpdir
. Tapi bagaimana dengan daerah lain? Bagaimana jika kita membutuhkan direktori sementara dari cakupan modul atau kelas? Untuk melakukan ini, kita membuat fixture lain dari ukuran wilayah yang diinginkan dan untuk ini kita harus menggunakan tmpdir_factory
.
Sebagai contoh, misalkan kita memiliki modul yang penuh dengan tes, dan banyak dari mereka harus dapat membaca beberapa data dari file json . Kami dapat menempatkan fixture volume fixture di modul itu sendiri, atau di file conftest.py , yang mengatur file data sebagai berikut:
ch4 / penulis / conftest.py
"""Demonstrate tmpdir_factory.""" import json import pytest @pytest.fixture(scope='module') def author_file_json(tmpdir_factory): """ .""" python_author_data = { 'Ned': {'City': 'Boston'}, 'Brian': {'City': 'Portland'}, 'Luciano': {'City': 'Sau Paulo'} } file = tmpdir_factory.mktemp('data').join('author_file.json') print('file:{}'.format(str(file))) with file.open('w') as f: json.dump(python_author_data, f) return file
author_file_json()
membuat direktori sementara bernama data dan membuat file bernama author_file.json di direktori data. Kemudian ia menulis kamus python_author_data
sebagai json . Karena ini adalah modul area fixture, file json akan dibuat hanya sekali untuk setiap modul menggunakan tes:
ch4 / penulis / test_authors.py
""" , .""" import json def test_brian_in_portland(author_file_json): """, .""" with author_file_json.open() as f: authors = json.load(f) assert authors['Brian']['City'] == 'Portland' def test_all_have_cities(author_file_json): """ .""" with author_file_json.open() as f: authors = json.load(f) for a in authors: assert len(authors[a]['City']) > 0
Kedua tes akan menggunakan file JSON yang sama. Jika satu file data uji berfungsi untuk beberapa tes, tidak masuk akal untuk membuatnya kembali untuk kedua tes.
Menggunakan pytestconfig
Menggunakan perlengkapan pytestconfig bawaan, Anda dapat mengontrol cara pytest bekerja dengan argumen dan opsi baris perintah, file konfigurasi, plugin, dan direktori tempat Anda meluncurkan pytest. Fixture pytestconfig adalah jalan pintas ke request.config, dan kadang-kadang disebut dalam dokumentasi pytest sebagai " objek konfigurasi pytest " (objek konfigurasi pytest).
Untuk mengetahui cara kerja pytestconfig , Anda dapat melihat cara menambahkan parameter baris perintah khusus dan membaca nilai parameter dari tes. Anda dapat membaca nilai parameter baris perintah langsung dari pytestconfig, tetapi untuk menambahkan parameter dan menganalisanya, Anda perlu menambahkan fungsi hook. Fungsi kait , yang saya jelaskan lebih terinci dalam Bab 5, “Plugin,” di halaman 95, adalah cara lain untuk mengontrol perilaku pytest dan sering digunakan dalam plugin. Namun, menambahkan opsi baris perintah khusus dan membacanya dari pytestconfig cukup luas, jadi saya ingin membahasnya di sini.
Kami akan menggunakan kait pytest_addoption
untuk menambahkan beberapa parameter ke parameter yang sudah tersedia di baris perintah pytest:
ch4 / pytestconfig / conftest.py
def pytest_addoption(parser): parser.addoption("--myopt", action="store_true", help="some boolean option") parser.addoption("--foo", action="store", default="bar", help="foo: bar or baz")
Menambahkan parameter baris perintah melalui pytest_addoption
harus dilakukan melalui plugins atau file conftest.py yang terletak di bagian atas struktur direktori proyek. Anda seharusnya tidak melakukan ini di subdirektori tes.
Opsi --myopt
dan --foo <value>
ditambahkan ke kode sebelumnya, dan baris bantuan diubah seperti yang ditunjukkan di bawah ini:
$ cd /path/to/code/ch4/pytestconfig $ pytest --help usage: pytest [options] [file_or_dir] [file_or_dir] [...] ... custom options: --myopt some boolean option --foo=FOO foo: bar or baz ...
Sekarang kita dapat mengakses opsi ini dari tes:
ch4 / pytestconfig / test_config.py
import pytest def test_option(pytestconfig): print('"foo" set to:', pytestconfig.getoption('foo')) print('"myopt" set to:', pytestconfig.getoption('myopt'))
Mari kita lihat cara kerjanya:
$ pytest -s -q test_config.py::test_option "foo" set to: bar "myopt" set to: False .1 passed in 0.01 seconds $ pytest -s -q --myopt test_config.py::test_option "foo" set to: bar "myopt" set to: True .1 passed in 0.01 seconds $ pytest -s -q --myopt --foo baz test_config.py::test_option "foo" set to: baz "myopt" set to: True .1 passed in 0.01 seconds
Karena pytestconfig adalah perlengkapan, ia juga bisa diperoleh dari perlengkapan lain. Anda dapat membuat fixture untuk nama opsi jika Anda mau, misalnya:
ch4 / pytestconfig / test_config.py
@pytest.fixture() def foo(pytestconfig): return pytestconfig.option.foo @pytest.fixture() def myopt(pytestconfig): return pytestconfig.option.myopt def test_fixtures_for_options(foo, myopt): print('"foo" set to:', foo) print('"myopt" set to:', myopt)
Anda juga dapat mengakses parameter bawaan, tidak hanya yang ditambahkan, serta informasi tentang cara meluncurkan pytest (direktori, argumen, dll.).
Berikut adalah contoh dari beberapa nilai dan opsi konfigurasi:
def test_pytestconfig(pytestconfig): print('args :', pytestconfig.args) print('inifile :', pytestconfig.inifile) print('invocation_dir :', pytestconfig.invocation_dir) print('rootdir :', pytestconfig.rootdir) print('-k EXPRESSION :', pytestconfig.getoption('keyword')) print('-v, --verbose :', pytestconfig.getoption('verbose')) print('-q, --quiet :', pytestconfig.getoption('quiet')) print('-l, --showlocals:', pytestconfig.getoption('showlocals')) print('--tb=style :', pytestconfig.getoption('tbstyle'))
Kami akan kembali ke pytestconfig ketika saya mendemonstrasikan file ini di bab 6, "Konfigurasi," di halaman 113.
Menggunakan cache
Biasanya, kami penguji berpikir bahwa setiap tes adalah independen mungkin dari tes lain. Anda harus memastikan bahwa dependensi akuntansi pesanan belum masuk. Saya ingin dapat menjalankan atau memulai kembali tes apa pun dalam urutan apa pun dan mendapatkan hasil yang sama. Selain itu, sesi tes harus berulang dan tidak mengubah perilaku berdasarkan sesi tes sebelumnya.
Namun, terkadang mentransfer informasi dari satu sesi tes ke sesi lainnya bisa sangat berguna. Saat kami ingin meneruskan informasi ke sesi pengujian selanjutnya, kami dapat melakukan ini dengan fixture cache
bawaan.
cache
fixture dirancang untuk menyimpan informasi tentang satu sesi tes dan mendapatkannya di sesi berikutnya. Contoh yang bagus untuk menggunakan izin cache
untuk kepentingan kasus ini adalah fungsionalitas built-in --last-failed
dan - --last-failed
--failed-first
. Mari kita lihat bagaimana data untuk flag ini disimpan dalam cache.
Berikut ini teks bantuan untuk opsi --last-failed
dan --failed-first
, serta beberapa opsi cache
:
$ pytest --help ... --lf, --last-failed rerun only the tests that failed at the last run (or all if none failed) --ff, --failed-first run all tests but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown --cache-show show cache contents, don t perform collection or tests --cache-clear remove all cache contents at start of test run. ...
Untuk melihatnya dalam aksi, kami akan menggunakan dua tes ini:
ch4 / cache / test_pass_fail.py
def test_this_passes(): assert 1 == 1 def test_this_fails(): assert 1 == 2
Mari kita jalankan mereka menggunakan --verbose
untuk melihat nama fungsi, dan --tb=no
untuk menyembunyikan jejak stack:
$ cd /path/to/code/ch4/cache $ pytest --verbose --tb=no test_pass_fail.py ==================== test session starts ==================== collected 2 items test_pass_fail.py::test_this_passes PASSED test_pass_fail.py::test_this_fails FAILED ============ 1 failed, 1 passed in 0.05 seconds =============
Jika Anda menjalankannya lagi dengan --ff
atau --failed-first
, maka tes yang gagal sebelumnya akan dieksekusi terlebih dahulu, dan kemudian seluruh sesi:
$ pytest --verbose --tb=no --ff test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures first collected 2 items test_pass_fail.py::test_this_fails FAILED test_pass_fail.py::test_this_passes PASSED ============ 1 failed, 1 passed in 0.04 seconds =============
Atau Anda dapat menggunakan --lf
atau --last-failed
untuk menjalankan tes yang gagal terakhir kali:
$ pytest --verbose --tb=no --lf test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures collected 2 items test_pass_fail.py::test_this_fails FAILED ==================== 1 tests deselected ===================== ========== 1 failed, 1 deselected in 0.05 seconds ===========
Sebelum kita melihat bagaimana data kerusakan disimpan dan bagaimana Anda dapat menggunakan mekanisme yang sama, mari kita lihat contoh lain yang membuat nilai --lf
dan --ff
bahkan lebih jelas.
Berikut ini adalah pengujian parameter dengan satu kegagalan:
ch4 / cache / test_few_failures.py
"""Demonstrate -lf and -ff with failing tests.""" import pytest from pytest import approx testdata = [
Dan pada output:
$ cd /path/to/code/ch4/cache $ pytest -q test_few_failures.py .F... ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) test_few_failures.py:17: AssertionError 1 failed, 4 passed in 0.06 seconds
Mungkin Anda dapat langsung mengidentifikasi masalahnya. Tetapi mari kita bayangkan bahwa tes itu lebih lama dan lebih rumit, dan tidak begitu jelas apa yang salah di sini. Mari kita jalankan tes lagi untuk melihat kesalahan lagi. Kasing uji dapat ditentukan pada baris perintah:
$ pytest -q "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]"
Jika Anda tidak ingin menyalin-tempel (salin / tempel ) atau ada beberapa kasus malang yang ingin Anda mulai ulang, maka - --lf
jauh lebih mudah. Dan jika Anda benar-benar --showlocals
-debug kegagalan pengujian, flag lain yang dapat memudahkan situasi adalah --showlocals
, atau -l
singkatnya:
$ pytest -q --lf -l test_few_failures.py F ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) expected = 1.1e+25 sum_ = 1.01e+25 x = 1e+25 y = 1e+23 test_few_failures.py:17: AssertionError ================= 4 tests deselected ================= 1 failed, 4 deselected in 0.05 seconds
Alasan kegagalan harus lebih jelas.
Untuk diingat bahwa tes gagal terakhir kali, ada sedikit trik. pytest menyimpan informasi kesalahan tes dari sesi tes terakhir dan Anda dapat melihat informasi yang disimpan dengan --cache-show
:
$ pytest --cache-show ===================== test session starts ====================== ------------------------- cache values ------------------------- cache/lastfailed contains: {'test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]': True} ================= no tests ran in 0.00 seconds =================
Atau Anda dapat melihat di direktori cache:
$ cat .cache/v/cache/lastfailed { "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]": true }
--clear-cache
memungkinkan Anda untuk menghapus cache sebelum sesi.
Cache dapat digunakan tidak hanya untuk --lf
dan --ff
. Mari kita menulis fixture yang mencatat berapa banyak waktu tes, menghemat waktu, dan waktu berikutnya melaporkan kesalahan dalam tes yang memakan waktu dua kali lebih lama daripada, katakanlah, terakhir kali.
Antarmuka untuk perlengkapan cache sederhana.
cache.get(key, default) cache.set(key, value)
Secara konvensional, nama-nama kunci dimulai dengan nama aplikasi atau plugin Anda, diikuti oleh /
, dan terus memisahkan bagian-bagian nama kunci dengan /
. Nilai yang Anda simpan bisa berupa apa saja yang dikonversi ke json , karena direpresentasikan dalam .cache directory
.
Berikut adalah perlengkapan kami yang digunakan untuk memperbaiki waktu tes:
ch4 / cache / test_slower.py
@pytest.fixture(autouse=True) def check_duration(request, cache): key = 'duration/' + request.node.nodeid.replace(':', '_') # (nodeid) # .cache # - start_time = datetime.datetime.now() yield stop_time = datetime.datetime.now() this_duration = (stop_time - start_time).total_seconds() last_duration = cache.get(key, None) cache.set(key, this_duration) if last_duration is not None: errorstring = " 2- " assert this_duration <= last_duration * 2, errorstring
Karena fixture adalah autouse , itu tidak perlu dirujuk dari tes. Objek permintaan digunakan untuk membuat simpulid untuk digunakan dalam kunci. simpulid adalah pengidentifikasi unik yang bekerja bahkan dengan tes parameter. Kami menambahkan kunci dengan 'durasi /' untuk menjadi penghuni cache yang terhormat. Kode di atas dijalankan sebelum fungsi tes; kode setelah hasil dijalankan setelah fungsi tes.
Sekarang kita perlu beberapa tes yang mengambil interval waktu yang berbeda:
ch4 / cache / test_slower.py
@pytest.mark.parametrize('i', range(5)) def test_slow_stuff(i): time.sleep(random.random())
Karena Anda mungkin tidak ingin menulis banyak tes untuk ini, saya menggunakan random
dan parameterisasi untuk dengan mudah menghasilkan beberapa tes yang tidur untuk jumlah waktu yang acak, semua lebih pendek dari satu detik. Mari kita lihat beberapa kali bagaimana ini bekerja:
$ cd /path/to/code/ch4/cache $ pytest -q --cache-clear test_slower.py ..... 5 passed in 2.10 seconds $ pytest -q --tb=line test_slower.py ...E..E =================================== ERRORS ==================================== ___________________ ERROR at teardown of test_slow_stuff[1] ___________________ E AssertionError: test duration over 2x last duration assert 0.35702 <= (0.148009 * 2) ___________________ ERROR at teardown of test_slow_stuff[4] ___________________ E AssertionError: test duration over 2x last duration assert 0.888051 <= (0.324019 * 2) 5 passed, 2 error in 3.17 seconds
Ya, itu menyenangkan. Mari kita lihat apa yang ada di cache:
$ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower.py::test_slow_stuff[2]': True, 'test_slower.py::test_slow_stuff[4]': True} cache\nodeids contains: ['test_slower.py::test_slow_stuff[0]', 'test_slower.py::test_slow_stuff[1]', 'test_slower.py::test_slow_stuff[2]', 'test_slower.py::test_slow_stuff[3]', 'test_slower.py::test_slow_stuff[4]'] cache\stepwise contains: [] duration\test_slower.py__test_slow_stuff[0] contains: 0.958055 duration\test_slower.py__test_slow_stuff[1] contains: 0.214012 duration\test_slower.py__test_slow_stuff[2] contains: 0.19001 duration\test_slower.py__test_slow_stuff[3] contains: 0.725041 duration\test_slower.py__test_slow_stuff[4] contains: 0.836048 no tests ran in 0.03 seconds
Anda dapat dengan mudah melihat data duration
secara terpisah dari data cache karena awalan nama data cache. Namun, sangat menarik bahwa fungsi lastfailed
dapat bekerja dengan satu entri cache. Data durasi kami membutuhkan satu entri cache untuk setiap pengujian. Mari kita ikuti contoh gagal terakhir dan letakkan data kita dalam satu catatan.
Kami membaca dan menyimpan untuk setiap tes. Kita dapat membagi fixture ke dalam fixture dari cakupan fungsi untuk mengukur durasi dan fixture dari cakupan sesi untuk membaca dan menulis ke cache. Namun, jika kita melakukan ini, kita tidak akan dapat menggunakan fixture cache karena ia memiliki cakupan fungsi. Untungnya, sekilas implementasi pada GitHub menunjukkan bahwa fixture caching hanya mengembalikan request.config.cache
. Ini tersedia di area mana pun.
:
ch4/cache/test_slower_2.py
Duration = namedtuple('Duration', ['current', 'last']) @pytest.fixture(scope='session') def duration_cache(request): key = 'duration/testdurations' d = Duration({}, request.config.cache.get(key, {})) yield d request.config.cache.set(key, d.current) @pytest.fixture(autouse=True) def check_duration(request, duration_cache): d = duration_cache nodeid = request.node.nodeid start_time = datetime.datetime.now() yield duration = (datetime.datetime.now() - start_time).total_seconds() d.current[nodeid] = duration if d.last.get(nodeid, None) is not None: errorstring = "test duration over 2x last duration" assert duration <= (d.last[nodeid] * 2), errorstring
duration_cache
. , , - . , namedtuple
Duration current
last
. namedtuple
test_duration
, . , namedtuple
, d.current
. .
, :
$ pytest -q --cache-clear test_slower_2.py ..... 5 passed in 2.80 seconds $ pytest -q --tb=no test_slower_2.py ...EE.. 7 passed, 2 error in 3.21 seconds $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower_2.py::test_slow_stuff[2]': True, 'test_slower_2.py::test_slow_stuff[3]': True} duration\testdurations contains: {'test_slower_2.py::test_slow_stuff[0]': 0.483028, 'test_slower_2.py::test_slow_stuff[1]': 0.198011, 'test_slower_2.py::test_slow_stuff[2]': 0.426024, 'test_slower_2.py::test_slow_stuff[3]': 0.762044, 'test_slower_2.py::test_slow_stuff[4]': 0.056003, 'test_slower_2.py::test_slow_stuff[5]': 0.18401, 'test_slower_2.py::test_slow_stuff[6]': 0.943054} no tests ran in 0.02 seconds
.
capsys
capsys builtin
: stdout stderr , . stdout stderr.
, stdout:
ch4/cap/test_capsys.py
def greeting(name): print('Hi, {}'.format(name))
, . - stdout. capsys:
ch4/cap/test_capsys.py
def test_greeting(capsys): greeting('Earthling') out, err = capsys.readouterr() assert out == 'Hi, Earthling\n' assert err == '' greeting('Brian') greeting('Nerd') out, err = capsys.readouterr() assert out == 'Hi, Brian\nHi, Nerd\n' assert err == ''
stdout
stderr
capsys.redouterr()
. — , , .
stdout
. , stderr
:
def yikes(problem): print('YIKES! {}'.format(problem), file=sys.stderr) def test_yikes(capsys): yikes('Out of coffee!') out, err = capsys.readouterr() assert out == '' assert 'Out of coffee!' in err
pytest . print . . -s
, stdout . , , . , pytest , , . capsys . capsys.disabled()
, .
Berikut ini sebuah contoh:
ch4/cap/test_capsys.py
def test_capsys_disabled(capsys): with capsys.disabled(): print('\nalways print this')
, 'always print this' :
$ cd /path/to/code/ch4/cap $ pytest -q test_capsys.py::test_capsys_disabled
, always print this
, capys.disabled()
. print — print, normal print, usually captured
( , ), , -s
, --capture=no
, .
monkeypatch
"monkey patch" — . "monkey patching" — , , . monkeypatch
. , , , , . , . API , monkeypatch .
monkeypatch
:
setattr(target, name, value=<notset>, raising=True)
: .delattr(target, name=<notset>, raising=True)
: .setitem(dic, name, value)
: .delitem(dic, name, raising=True)
: .setenv(name, value, prepend=None)
: .delenv(name, raising=True)
: .syspath_prepend(path)
: sys., Python.chdir(path)
: .
raising
pytest, , . prepend
setenv()
. , + prepend + <old value>
.
monkeypatch , , dot- . , dot- . , cheese- :
ch4/monkey/cheese.py
import os import json def read_cheese_preferences(): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'r') as f: prefs = json.load(f) return prefs def write_cheese_preferences(prefs): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'w') as f: json.dump(prefs, f, indent=4) def write_default_cheese_preferences(): write_cheese_preferences(_default_prefs) _default_prefs = { 'slicing': ['manchego', 'sharp cheddar'], 'spreadable': ['Saint Andre', 'camembert', 'bucheron', 'goat', 'humbolt fog', 'cambozola'], 'salads': ['crumbled feta'] }
, write_default_cheese_preferences()
. , . , . .
, . , read_cheese_preferences()
, , write_default_cheese_preferences()
:
ch4/monkey/test_cheese.py
def test_def_prefs_full(): cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
, , , cheese- . .
HOME set
, os.path.expanduser()
~
, HOME
. HOME
, :
ch4/monkey/test_cheese.py
def test_def_prefs_change_home(tmpdir, monkeypatch): monkeypatch.setenv('HOME', tmpdir.mkdir('home')) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
, HOME
. - expanduser()
, , «On Windows, HOME and USERPROFILE will be used if set, otherwise a combination of….» . Wow! , Windows. , .
, HOME
, expanduser
:
ch4/monkey/test_cheese.py
def test_def_prefs_change_expanduser(tmpdir, monkeypatch): fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
, cheese os.path.expanduser()
-. re.sub
~
. setenv()
setattr()
. , setitem()
.
, , , . , , write_default_cheese_preferences()
:
ch4/monkey/test_cheese.py
def test_def_prefs_change_defaults(tmpdir, monkeypatch):
_default_prefs
- , monkeypatch.setitem()
, .
setenv()
, setattr()
setitem()
. del
. , , - . monkeypatch
.
syspath_prepend(path)
sys.path
, , . stub-. monkeypatch.syspath_prepend()
, , stub-.
chdir(path)
. , . , , monkeypatch.chdir(the_tmpdir)
.
monkeypatch unittest.mock
, . 7 " pytest " . 125.
doctest_namespace
doctest Python docstrings , , . pytest doctest Python --doctest-modules
. doctest_namespace
, autouse , pytest doctest . docstrings .
doctest_namespace
, Python . , numpy
import numpy as np
.
. , unnecessary_math.py
multiply()
divide()
, . , docstring , docstrings :
ch4/dt/1/unnecessary_math.py
""" This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> um.divide(10, 5) 2.0 """ return a / b
unnecessary_math
, um
, import noecessary_math as um
-. docstrings import
, um
. , pytest docstring . docstring , docstrings :
$ cd /path/to/code/ch4/dt/1 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED unnecessary_math.py::unnecessary_math.divide FAILED unnecessary_math.py::unnecessary_math.multiply FAILED ================================== FAILURES =================================== ______________________ [doctest] unnecessary_math.divide ______________________ 034 035 Returns a divided by b. 036 037 >>> um.divide(10, 5) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.divide[0]>", line 1, in <module> NameError: name 'um' is not defined ... _____________________ [doctest] unnecessary_math.multiply _____________________ 022 023 Returns a multiplied by b. 024 025 >>> um.multiply(4, 3) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.multiply[0]>", line 1, in <module> NameError: name 'um' is not defined /path/to/code/ch4/dt/1/unnecessary_math.py:23: UnexpectedException ================ 2 failed, 1 passed in 0.03 seconds =================
- import
docstring:
ch4/dt/2/unnecessary_math.py
""" This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> import unnecessary_math as um >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> import unnecessary_math as um >>> um.divide(10, 5) 2.0 """ return a / b
:
$ cd /path/to/code/ch4/dt/2 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED [ 33%] unnecessary_math.py::unnecessary_math.divide PASSED [ 66%] unnecessary_math.py::unnecessary_math.multiply PASSED [100%] ===================== 3 passed in 0.03 seconds ======================
docstrings .
doctest_namespace
, autouse
conftest.py
, :
ch4/dt/3/conftest.py
import pytest import unnecessary_math @pytest.fixture(autouse=True) def add_um(doctest_namespace): doctest_namespace['um'] = unnecessary_math
pytest um
doctest_namespace
, unnecessary_math
. conftest.py, doctests, conftest.py um
.
doctest pytest 7 " pytest " . 125.
recwarn
recwarn
, . Python , , , . , , , , . :
ch4/test_warnings.py
import warnings import pytest def lame_function(): warnings.warn("Please stop using this", DeprecationWarning)
, :
ch4/test_warnings.py
def test_lame_function(recwarn): lame_function() assert len(recwarn) == 1 w = recwarn.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this'
recwarn , category
(), message
(), filename
( ) lineno
( ), .
. , , , . recwarn.clear()
, , .
recwarn
, pytest pytest.warns()
:
ch4/test_warnings.py
def test_lame_function_2(): with pytest.warns(None) as warning_list: lame_function() assert len(warning_list) == 1 w = warning_list.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this'
pytest.warns()
. recwarn
pytest.warns()
, , , .
Latihan
ch4/cache/test_slower.py
autouse
, check_duration()
. ch3/tasks_proj/tests/conftest.py
.- Lakukan tes pada Bab 3.
- Untuk tes sangat cepat, 2x sangat cepat masih sangat cepat. Alih-alih 2x, ubah fixture untuk memeriksa 0,1 detik plus 2x untuk durasi terakhir.
- Jalankan pytest dengan fixture yang dimodifikasi. Apakah hasilnya tampak masuk akal?
Apa selanjutnya
Dalam bab ini, Anda melihat beberapa perlengkapan pytest bawaan. Selanjutnya, Anda akan mempertimbangkan Plugin lebih detail. Nuansa menulis plugin besar bisa menjadi buku tersendiri; namun, plugin khusus berukuran kecil adalah bagian rutin dari ekosistem terbaik.
Kembali Selanjutnya 