Parameterisasi dari file di py.test

Di bidang pengujian otomatis, Anda dapat menemukan alat yang berbeda, misalnya, py.test adalah salah satu solusi paling populer untuk menulis tes otomatis dengan Python.


Setelah melalui banyak sumber daya yang terkait dengan pytest dan setelah mempelajari dokumentasi dari situs resmi proyek, saya tidak dapat menemukan deskripsi langsung dari solusi untuk salah satu tugas utama - menjalankan tes dengan data uji yang disimpan dalam file terpisah. Kalau tidak, dapat dikatakan, pemuatan parameter ke dalam fungsi uji dari file (s) atau parameterisasi dari file secara langsung. Prosedur seperti itu tidak dijelaskan di mana saja dalam seluk-beluk dan satu-satunya penyebutan fitur ini hanya dalam satu baris dari dokumentasi pytest.


Pada artikel ini saya akan berbicara tentang solusi saya untuk masalah ini.




Tantangan


Tugas utama adalah untuk menghasilkan kasus uji dalam bentuk parameter test_input dan expected_result ke masing-masing fungsi tes individu dari nama fungsi file yang sesuai.


Tugas tambahan:


  • pilih format file yang dapat dibaca manusia dengan kasus uji;
  • meninggalkan kemampuan untuk mendukung kasus uji hard-coded;
  • menampilkan pengidentifikasi yang jelas untuk setiap kasus.

Toolkit


Dalam artikel tersebut, saya menggunakan Python 3 (2.7 juga cocok), pyyaml, dan pytest (versi 5+ untuk Python 3, atau 4.6 untuk Python 2.7) tanpa menggunakan plugin pihak ketiga. Selain itu, pustaka os standar akan digunakan.


File itu sendiri dari mana kita akan mengambil kasus uji perlu disusun menggunakan bahasa markup yang nyaman bagi pemahaman manusia. Dalam kasus saya, YAML dipilih (karena itu menyelesaikan tugas tambahan memilih format yang dapat dibaca manusia) . Bahkan, bahasa markup seperti apa untuk file dengan set data yang Anda butuhkan tergantung pada persyaratan yang disajikan pada proyek.




Implementasi


Karena pilar utama alam semesta dalam pemrograman adalah kesepakatan, kita harus memperkenalkan beberapa darinya untuk solusi kita.


Intersepsi


Untuk mulai dengan, solusi ini menggunakan fungsi intersepsi pytest_generate_tests ( wiki ), yang dimulai pada tahap menghasilkan kasus uji, dan metafunc argumennya, yang memungkinkan kita untuk parameterisasi fungsi. Pada titik ini, pytest mengulangi setiap fungsi tes dan menjalankan kode generasi selanjutnya untuk itu.


Argumen


Anda harus menentukan daftar lengkap parameter untuk fungsi tes. Dalam kasus saya, kamusnya adalah test_input dan semua tipe data (paling sering berupa string atau integer) dalam expected_result . Kami membutuhkan parameter ini untuk digunakan di metafunc.parametrize(...) .


Parameterisasi


Fungsi ini sepenuhnya mengulangi operasi @pytest.mark.parametrize parameterisasi @pytest.mark.parametrize , yang mengambil argumen pertama string yang mencantumkan argumen fungsi pengujian (dalam kasus kami "test_input, expected_result" ) dan daftar data yang akan digunakan untuk membuat kasus pengujian kami (misalnya, [(1, 2), (2, 4), (3, 6)] )).


Dalam pertempuran, akan terlihat seperti ini:


 @pytest.mark.parametrize("test_input, expected_result", [(1, 2), (2, 4), (3, 6)]) def test_multiplication(test_input, expected_result): assert test_input * 2 == expected_result 

Dan dalam kasus kami, kami akan menunjukkan ini sebelumnya:


 #  ... return metafunc.parametrize("test_input, expected", test_cases) #  `[(1, 2), (2, 4), (3, 6)]` 

Penyaringan


Dari sini juga mengikuti alokasi fungsi-fungsi tes di mana data dari file diperlukan, dari mereka yang menggunakan data statis / dinamis. Kami akan menerapkan penyaringan ini sebelum mengurai informasi dari file.


Filter itu sendiri bisa berupa apa saja, misalnya:


  • Penanda fungsi bernama yaml :

 #     -  if not hasattr(metafunc.function, 'pytestmark'): return #            mark_names = [ mark.name for mark in metafunc.function.pytestmark ] #   ,        if 'yaml' not in mark_names: return 

Jika tidak, filter yang sama dapat diterapkan seperti ini:


 #           if Mark(name='yaml', args=(), kwargs={}) not in metafunc.function.pytestmark: return 

  • Argumen ke fungsi test_input :

 #   ,     test_input if 'test_input' not in metafunc.fixturenames: return 

Opsi ini paling cocok untuk saya.




Hasil


Kita hanya perlu menambahkan bagian di mana kita mengurai data dari file. Ini tidak akan sulit dalam kasus yaml (dan juga json, xml, dll.) , Jadi kami mengumpulkan semuanya ke heap.


 # conftest.py import os import yaml import pytest def pytest_generate_tests(metafunc): #   ,     test_input if 'test_input' not in metafunc.fixturenames: return #     dir_path = os.path.dirname(os.path.abspath(metafunc.module.__file__)) #       file_path = os.path.join(dir_path, metafunc.function.__name__ + '.yaml') #    with open(file_path) as f: test_cases = yaml.full_load(f) #       if not test_cases: raise ValueError("Test cases not loaded") return metafunc.parametrize("test_input, expected_result", test_cases) 

Kami menulis skrip pengujian seperti ini:


 # test_script.py import pytest def test_multiplication(test_input, expected_result): assert test_input * 2 == expected_result 

File data:


 # test_multiplication.yaml - !!python/tuple [1,2] - !!python/tuple [1,3] - !!python/tuple [1,5] - !!python/tuple [2,4] - !!python/tuple [3,4] - !!python/tuple [5,4] 

Kami mendapatkan daftar kasus uji berikut:


  pytest /test_script.py --collect-only ======================== test session starts ======================== platform linux -- Python 3.7.4, pytest-5.2.1, py-1.8.0, pluggy-0.13.0 rootdir: /pytest_habr collected 6 items <Module test_script.py> <Function test_multiplication[1-2]> <Function test_multiplication[1-3]> <Function test_multiplication[1-5]> <Function test_multiplication[2-4]> <Function test_multiplication[3-4]> <Function test_multiplication[5-4]> ======================== no tests ran in 0.04s ======================== 

Dan dengan menjalankan skrip, hasil ini: 4 failed, 2 passed, 1 warnings in 0.11s




Tambah. tugas


Ini bisa menjadi akhir artikel, tetapi untuk kompleksitas yang lebih besar, saya akan menambahkan pengidentifikasi yang lebih nyaman untuk fungsi kita, data lain yang menguraikan dan menandai setiap kasus uji individu.


Jadi, segera, kode:


 # conftest.py import os import yaml import pytest def pytest_generate_tests(metafunc): def generate_id(input_data, level): level += 1 #      INDENTS = { # level: (levelmark, addition_indent) 1: ('_', ['', '']), 2: ('-', ['[', ']']) } COMMON_INDENT = ('-', ['[', ']']) levelmark, additional_indent = INDENTS.get(level, COMMON_INDENT) #     -     if level > 3: return additional_indent[0] + type(input_data).__name__ + additional_indent[1] #    elif isinstance(input_data, (str, bool, float, int)): return str(input_data) #   elif isinstance(input_data, (list, set, tuple)): #   ,    ,   list_repr = levelmark.join( [ generate_id(input_value, level=level) \ for input_value in input_data ]) return additional_indent[0] + list_repr + additional_indent[1] #      elif isinstance(input_data, dict): return '{' + levelmark.join(input_data.keys()) + '}' #     else: return None #   ,     test_input if 'test_input' not in metafunc.fixturenames: return #     dir_path = os.path.dirname(os.path.abspath(metafunc.module.__file__)) #       file_path = os.path.join(dir_path, metafunc.function.__name__ + '.yaml') #    with open(file_path) as f: raw_test_cases = yaml.full_load(f) #       if not raw_test_cases: raise ValueError("Test cases not loaded") #    - test_cases = [] #      for case_id, test_case in enumerate(raw_test_cases): #    marks = [ getattr(pytest.mark, name) for name in test_case.get("marks", []) ] #    ,   case_id = test_case.get("id", generate_id(test_case["test_data"], level=0)) #         pytest.param test_cases.append(pytest.param(*test_case["test_data"], marks=marks, id=case_id)) return metafunc.parametrize("test_input, expected_result", test_cases) 

Karenanya, kami mengubah tampilan file YAML kami:


 # test_multiplication.yaml - test_data: [1, 2] id: 'one_two' - test_data: [1,3] marks: ['xfail'] - test_data: [1,5] marks: ['skip'] - test_data: [2,4] id: "it's good" marks: ['xfail'] - test_data: [3,4] marks: ['negative'] - test_data: [5,4] marks: ['more_than'] 

Maka uraian akan berubah menjadi:


 <Module test_script.py> <Function test_multiplication[one_two]> <Function test_multiplication[1_3]> <Function test_multiplication[1_5]> <Function test_multiplication[it's good]> <Function test_multiplication[3_4]> <Function test_multiplication[5_4]> 

Dan peluncurannya adalah: 2 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 2 warnings in 0.12s


PS: peringatan - karena spidol yang ditulis sendiri tidak dicatat di pytest.ini


Dalam pengembangan topik


Siap untuk berdiskusi di komentar pertanyaan tentang jenis:


  • apa cara terbaik untuk menulis file yaml?
  • Dalam format apa lebih nyaman menyimpan data uji?
  • Kasus uji tambahan apa yang dibutuhkan pada tahap pembuatan?
  • Apakah saya perlu pengidentifikasi untuk setiap kasus?

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


All Articles