Pengujian Python dengan pytest. Menggunakan pytest dengan alat lain, BAB 7

Kembali


Biasanya, pytest tidak digunakan secara independen, tetapi dalam lingkungan pengujian dengan alat lain. Bab ini membahas alat-alat lain yang sering digunakan bersama dengan pytest untuk pengujian yang efektif dan efisien. Meskipun ini bukan daftar lengkap, alat-alat yang dibahas di sini akan memberi Anda gambaran tentang rasa kekuatan mencampur pytest dengan alat-alat lainnya.



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.



pdb: Kegagalan Uji Debugging


Modul pdb adalah debugger Python di perpustakaan standar. Anda menggunakan --pdb sehingga pytest memulai sesi debug pada titik kegagalan. Mari kita lihat pdb dalam aksi dalam konteks proyek Tugas.


Dalam "Parameterisasi Fixture" pada halaman 64, kami meninggalkan proyek Tugas dengan beberapa kesalahan:


 $ cd /path/to/code/ch3/c/tasks_proj $ pytest --tb=no -q .........................................FF.FFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF........... 42 failed, 54 passed in 4.74 seconds 

Sebelum kita melihat bagaimana pdb dapat membantu kita men-debug tes ini, mari kita lihat opsi pytest yang tersedia untuk mempercepat debugging kesalahan tes, yang pertama kali kita bahas di bagian "Menggunakan Pilihan" pada halaman 9:


  • --tb=[auto/long/short/line/native/no] : Mengontrol gaya jejak.
  • -v / --verbose : Menampilkan semua nama uji yang lulus atau gagal.
  • -l / --showlocals : Menampilkan variabel lokal di sebelah jejak stack.
  • -lf / --last-failed : Menjalankan hanya tes yang gagal.
  • -x / --exitfirst : Menghentikan sesi tes pada kegagalan pertama.
  • --pdb : Memulai sesi debugging interaktif pada titik kegagalan.



Menginstal MongoDB




Seperti disebutkan dalam Bab 3, "Perlengkapan Pytest," pada halaman 49, instalasi MongoDB dan pymongo diperlukan untuk menjalankan tes MongoDB.


Saya menguji versi Server Komunitas yang ditemukan di https://www.mongodb.com/download-center . instal pymongo dengan pip : pip install pymongo . Namun, ini adalah contoh terakhir dalam buku yang menggunakan MongoDB. Untuk mencoba debugger tanpa menggunakan MongoDB, Anda dapat menjalankan perintah pytest dari code/ch2/ , karena direktori ini juga berisi beberapa tes gagal.




Kami hanya menjalankan tes dari code/ch3/c untuk memastikan beberapa dari mereka tidak berfungsi. Kami tidak melihat traceback atau nama uji karena --tb=no menonaktifkan penelusuran dan kami tidak mengaktifkan --verbose . Mari kita ulangi kesalahan (tidak lebih dari tiga) dengan teks terperinci:


 $ pytest --tb=no --verbose --lf --maxfail=3 ============================= test session starts ============================= collected 96 items / 52 deselected run-last-failure: rerun previous 44 failures tests/func/test_add.py::test_add_returns_valid_id[mongo] ERROR [ 2%] tests/func/test_add.py::test_added_task_has_id_set[mongo] ERROR [ 4%] tests/func/test_add.py::test_add_increases_count[mongo] ERROR [ 6%] =================== 52 deselected, 3 error in 0.72 seconds ==================== 

Sekarang kita tahu tes mana yang gagal. Mari kita lihat salah satunya, menggunakan -x , menyalakan tracing, tidak menggunakan --tb=no , dan menampilkan variabel lokal dengan -l :


 $ pytest -v --lf -l -x ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED =========================== FAILURES =========================== _______________ test_add_returns_valid_id[mongo] _______________ tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # 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) E AssertionError: assert False E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int) new_task = Task(summary='do something', owner=None, done=False, id=None) task_id = ObjectId('59783baf8204177f24cb1b68') tasks_db = None tests/func/test_add.py:16: AssertionError !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== =========== 1 failed, 54 deselected in 2.47 seconds ============ 

Cukup sering ini cukup untuk memahami mengapa tes gagal. Dalam kasus khusus ini, cukup jelas bahwa task_id bukan bilangan bulat - itu adalah instance dari ObjectId. ObjectId adalah tipe yang digunakan oleh MongoDB untuk pengidentifikasi objek dalam database. Niat saya dengan lapisan tasksdb_pymongo.py adalah untuk menyembunyikan detail tertentu dari implementasi MongoDB dari sisa sistem. Jelas bahwa dalam hal ini tidak berhasil.


Namun, kami ingin melihat cara menggunakan pdb dengan pytest, jadi mari kita bayangkan tidak jelas mengapa tes ini gagal. Kita dapat membuat pytest memulai sesi debugging dan memulai kita tepat pada titik kegagalan menggunakan --pdb :


 $ pytest -v --lf -x --pdb ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED >>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>> tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # 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) E AssertionError: assert False E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int) tests/func/test_add.py:16: AssertionError >>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>> > /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16) > test_add_returns_valid_id() -> assert isinstance(task_id, int) (Pdb) 

Sekarang kita berada di prompt (Pdb), kita memiliki akses ke semua fitur debugging pdb interaktif. Saat melihat crash, saya secara teratur menggunakan perintah ini:


  • p/print expr : Mencetak nilai exp.
  • pp expr : Pretty mencetak nilai expr.
  • l/list : Daftar titik kegagalan dan lima baris kode di atas dan di bawah.
  • l/list begin,end : Menghitung angka garis tertentu.
  • a/args : Mencetak argumen fungsi saat ini dengan nilainya.
  • u/up : Memindahkan satu tingkat ke atas jalur tumpukan.
  • d/down : Bergerak ke bawah satu tingkat dalam jejak tumpukan.
  • q/quit : Mengakhiri sesi debugging.

Perintah navigasi lainnya, seperti langkah dan selanjutnya, tidak terlalu berguna, karena kita duduk tepat di pernyataan tegas. Anda juga bisa cukup memasukkan nama variabel dan mendapatkan nilai.


Anda dapat menggunakan p/print expr mirip dengan opsi -l/--showlocals untuk melihat nilai-nilai dalam suatu fungsi:


 (Pdb) p new_task Task(summary='do something', owner=None, done=False, id=None) (Pdb) p task_id ObjectId('59783bf48204177f2a786893') (Pdb) 

Sekarang Anda dapat keluar dari debugger dan melanjutkan pengujian.


 (Pdb) q !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== ========== 1 failed, 54 deselected in 123.40 seconds =========== 

Jika kami tidak menggunakan - , pytest akan membuka Pdb lagi di tes berikutnya. Informasi lebih lanjut tentang penggunaan modul pdb tersedia di dokumentasi Python .


Coverage.py: Menentukan jumlah kode uji


Cakupan kode adalah indikator persentase kode yang diuji yang diuji oleh serangkaian tes. Ketika Anda menjalankan tes untuk proyek Tugas, beberapa fungsi Tugas dieksekusi dengan setiap tes, tetapi tidak dengan semua.


Alat cakupan kode sangat bagus untuk memberi tahu Anda bagian mana dari sistem yang sepenuhnya diabaikan oleh pengujian.


Coverage.py adalah alat cakupan Python pilihan yang mengukur cakupan kode.


Anda akan menggunakannya untuk memverifikasi kode proyek Tugas dengan pytest.


Untuk menggunakan coverage.py Anda harus menginstalnya. Tidak pytest-cov salahnya menginstal plugin bernama pytest-cov , yang memungkinkan Anda untuk memanggil coverage.py dari pytest dengan beberapa opsi pytest tambahan. Karena coverage adalah salah satu dependensi pytest-cov , cukup instal pytest-cov dan itu akan mengambil coverage.py :


 $ pip install pytest-cov Collecting pytest-cov Using cached pytest_cov-2.5.1-py2.py3-none-any.whl Collecting coverage>=3.7.1 (from pytest-cov) Using cached coverage-4.4.1-cp36-cp36m-macosx_10_10_x86 ... Installing collected packages: coverage, pytest-cov Successfully installed coverage-4.4.1 pytest-cov-2.5.1 

Mari kita jalankan laporan cakupan untuk versi tugas kedua. Jika Anda masih memiliki versi pertama proyek Tugas yang diinstal, hapus instalannya dan instal versi 2:


 $ pip uninstall tasks Uninstalling tasks-0.1.0: /path/to/venv/bin/tasks /path/to/venv/lib/python3.6/site-packages/tasks.egg-link Proceed (y/n)? y Successfully uninstalled tasks-0.1.0 $ cd /path/to/code/ch7/tasks_proj_v2 $ pip install -e . Obtaining file:///path/to/code/ch7/tasks_proj_v2 ... Installing collected packages: tasks Running setup.py develop for tasks Successfully installed tasks $ pip list ... tasks (0.1.1, /path/to/code/ch7/tasks_proj_v2/src) ... 

Sekarang versi tugas berikutnya diinstal, Anda dapat menjalankan laporan cakupan dasar:


 $ cd /path/to/code/ch7/tasks_proj_v2 $ pytest --cov=src ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Name Stmts Miss Cover -------------------------------------------------- src\tasks\__init__.py 2 0 100% src\tasks\api.py 79 22 72% src\tasks\cli.py 45 14 69% src\tasks\config.py 18 12 33% src\tasks\tasksdb_pymongo.py 74 74 0% src\tasks\tasksdb_tinydb.py 32 4 88% -------------------------------------------------- TOTAL 250 126 50% ================== 62 passed in 0.47 seconds =================== 

Karena direktori saat ini adalah tasks_proj_v2 , dan kode sumber yang diuji dalam src, menambahkan opsi --cov=src menghasilkan laporan cakupan untuk direktori itu saja.


Seperti yang Anda lihat, beberapa file memiliki cakupan yang sangat rendah dan bahkan 0%. Ini adalah pengingat yang berguna: tasksdb_pymongo.py 0% karena kami telah menonaktifkan pengujian untuk MongoDB dalam versi ini. Beberapa dari mereka cukup rendah. Proyek ini tentu harus memberikan tes untuk semua bidang ini sebelum siap untuk prime time.


Saya percaya bahwa beberapa file memiliki persentase cakupan yang lebih tinggi: api.py dan tasksdb_tinydb.py . Mari kita lihat tasksdb_tinydb.py dan lihat apa yang hilang. Saya pikir cara terbaik untuk melakukan ini adalah dengan menggunakan laporan HTML.


Jika Anda menjalankan coverage.py lagi dengan opsi --cov-report=html , --cov-report=html akan dihasilkan:


 $ pytest --cov=src --cov-report=html ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Coverage HTML written to dir htmlcov ================== 62 passed in 0.45 seconds =================== 

Anda kemudian dapat membuka htmlcov/index.html di browser yang menampilkan output di layar berikut:



Mengklik pada tasksdb_tinydb.py akan menampilkan laporan untuk satu file. Persentase garis yang tercakup ditampilkan di bagian atas laporan, ditambah berapa banyak garis yang dicakup dan berapa banyak yang tidak, seperti yang ditunjukkan pada layar berikutnya:



Menggulir ke bawah, Anda dapat melihat garis yang hilang, seperti yang ditunjukkan pada layar berikut:



Bahkan jika layar ini bukan halaman penuh untuk file ini, ini cukup untuk memberi tahu kami bahwa:


  1. Kami tidak menguji list_tasks() dengan pemilik yang ditetapkan.
  2. Kami tidak menguji update() atau delete() .
  3. Mungkin kami tidak sepenuhnya menguji unique_id() .

Bagus Kami dapat memasukkannya dalam daftar pengujian TO-DO bersama dengan pengujian sistem konfigurasi.


Meskipun alat cakupan kode sangat berguna, memperjuangkan cakupan 100% bisa berbahaya. Ketika Anda melihat kode yang tidak sedang diuji, itu mungkin berarti bahwa tes diperlukan. Tetapi bisa juga berarti bahwa ada beberapa fungsi sistem yang tidak diperlukan dan dapat dihapus. Seperti semua alat pengembangan perangkat lunak, analisis cakupan kode tidak menggantikan pemikiran.


Lihat coverage.py dan pytest-cov detail lebih lanjut.


mock: Pergantian bagian-bagian sistem


Paket tiruan digunakan untuk mengganti bagian-bagian dari sistem untuk mengisolasi bagian-bagian dari kode uji dari sisa sistem. Mock - objek kadang-kadang disebut test doubles, spies, fakes atau stub.


Di antara perlengkapan monkeypatch pytest Anda sendiri (dijelaskan dalam Menggunakan monkeypatch pada halaman 85) dan tiruan, Anda harus memiliki semua fungsionalitas uji ganda yang diperlukan.


Perhatian! Mock dan sangat aneh
Jika ini adalah pertama kalinya Anda menemukan kembar uji seperti mengejek, bertopik, dan mata-mata, bersiap-siaplah! Akan sangat aneh sangat cepat, lucu, meskipun sangat mengesankan.

Paket mock hadir dengan pustaka Python standar, seperti unittest.mock sejak Python 3.3. Dalam versi sebelumnya, ini tersedia sebagai paket terpisah yang diinstal melalui PyPI. Ini artinya Anda bisa menggunakan versi mock PyPI dari Python 2.6 ke versi Python terbaru dan mendapatkan fungsi yang sama dengan mock Python terbaru. Namun, untuk digunakan dengan pytest, sebuah plugin bernama pytest-mock memiliki beberapa fitur yang menjadikannya antarmuka pilihan saya untuk sistem mock.


Untuk proyek Tugas, kami akan menggunakan mock untuk membantu kami menguji antarmuka baris perintah. Di Coverage.py: dengan menentukan berapa banyak kode yang diuji, pada halaman 129 Anda melihat bahwa file cli.py kami tidak diuji sama sekali. Kami akan mulai memperbaikinya sekarang. Tapi mari kita bicara tentang strategi dulu.


Solusi pertama dalam proyek Tugas adalah melakukan sebagian besar pengujian fungsionalitas melalui api.py Oleh karena itu, solusi yang masuk akal adalah pengujian baris perintah tidak harus pengujian fungsional penuh. Kita dapat yakin bahwa sistem akan bekerja melalui CLI jika kita mendapatkan level API basah selama pengujian CLI. Ini juga merupakan solusi mudah yang memungkinkan kita untuk melihat moki di bagian ini.


Implementasi Tugas CLI menggunakan paket antarmuka baris perintah Klik pihak ketiga. Ada banyak alternatif untuk mengimplementasikan antarmuka baris perintah, termasuk modul yang dibangun ke dalam Python argparse . Salah satu alasan saya memilih Click adalah karena ia menyertakan mesin uji yang membantu kami menguji aplikasi Click. Namun, kode di cli.py , meskipun kami berharap tipikal aplikasi Click, tidak jelas.


Mari memperlambat dan menginstal Tugas ke-3 versi:


 $ cd /path/to/code/ $ pip install -e ch7/tasks_proj_v2 ... Successfully installed tasks 

Di sisa bagian ini, Anda akan mengembangkan beberapa tes untuk menguji fungsionalitas "daftar".
Mari kita lihat aksi untuk memahami apa yang akan kita periksa:


Catatan Penerjemah: Saat menggunakan platform Windows, saya menemui beberapa masalah saat menguji sesi di bawah ini.
  1. Folder harus dibuat untuk basis data bernama tasks_db di folder pengguna Anda. Sebagai contoh c:\Users\User_1\tasks_db\
    Kalau tidak, kita mendapatkan - >> FileNotFoundError: [Errno 2] Tidak ada file atau direktori seperti itu: 'c: \ Users \ User_1 // task_db // task_db.json'
  2. Gunakan tanda kutip ganda sebagai ganti tanda kutip. Kalau tidak, dapatkan kesalahan
    'melakukan sesuatu yang hebat'
    Penggunaan: tugas menambahkan [OPSI] RINGKASAN
    Coba "tugas tambahkan -h" untuk bantuan.

    Kesalahan: Mendapat argumen tambahan yang tidak terduga (sesuatu yang hebat ')


 $ tasks list ID owner done summary -- ----- ---- ------- $ tasks add 'do something great' $ tasks add "repeat" -o Brian $ tasks add "again and again" --owner Okken $ tasks list ID owner done summary -- ----- ---- ------- 1 False do something great 2 Brian False repeat 3 Okken False again and again $ tasks list -o Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat $ tasks list --owner Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat 

Ini terlihat sangat sederhana. Perintah tasks list menampilkan daftar semua tugas di bawah judul.
Judul dicetak meskipun daftar kosong. Perintah hanya menampilkan data dari satu pemilik, jika -o atau --owner . Dan bagaimana kita memeriksanya? Ada banyak cara, tetapi kita akan menggunakan moki.


Tes yang menggunakan MOK adalah tes kotak putih , dan kita perlu melihat kode untuk memutuskan apa dan di mana kita akan pergi. Titik masuk utama ada di sini:


ch7 / task_proj_v2 / src / task / cli.py

 if __name__ == '__main__': tasks_cli() 

Ini hanya panggilan ke tasks_cli() :


ch7 / task_proj_v2 / src / task / cli.py

 @click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.version_option(version='0.1.1') def tasks_cli(): """Run the tasks application.""" pass 

Jelas? Tidak. Tapi tunggu, itu menjadi baik (atau buruk, tergantung pada sudut pandang Anda). Ini adalah salah satu dari list perintah:


ch7 / task_proj_v2 / src / task / cli.py

 @tasks_cli.command(name="list", help="list tasks") @click.option('-o', '--owner', default=None, help='list tasks with this owner') def list_tasks(owner): """    .   ,      . """ formatstr = "{: >4} {: >10} {: >5} {}" print(formatstr.format('ID', 'owner', 'done', 'summary')) print(formatstr.format('--', '-----', '----', '-------')) with _tasks_db(): for t in tasks.list_tasks(owner): done = 'True' if t.done else 'False' owner = '' if t.owner is None else t.owner print(formatstr.format( t.id, owner, done, t.summary)) 

Ketika Anda terbiasa menulis kode Klik, pastikan kode ini tidak terlalu buruk. Saya tidak akan menjelaskan di sini apa dan bagaimana cara kerjanya dalam fungsi ini, karena pengembangan kode baris perintah bukanlah fokus buku; Namun, meskipun saya hampir sepenuhnya yakin bahwa saya memiliki kode yang benar ini, bagaimanapun, selalu ada banyak ruang untuk kesalahan manusia. Itu sebabnya serangkaian tes otomatis yang baik penting untuk memastikan bahwa fitur ini berfungsi dengan benar.
list_tasks(owner) tergantung pada beberapa fungsi lain: tasks_db() , yang merupakan manajer konteks, dan tasks.list_tasks(owner) , yang merupakan fungsi API.


Kita akan menggunakan mock untuk meletakkan fungsi palsu di tempat untuk tasks_db() dan tasks.list_tasks() . Kemudian kita dapat memanggil metode list_tasks melalui antarmuka baris perintah dan memastikan bahwa ia memanggil fungsi tasks.list_tasks() , yang berfungsi dengan benar dan benar memproses nilai kembali.
Untuk menghilangkan tasks_db() , mari kita lihat implementasi nyata:


Kembali

Source: https://habr.com/ru/post/id448798/


All Articles