Banyak perpustakaan populer untuk pengujian, misalnya Google Test, Catch2, Boost.Test, sangat terkait dengan penggunaan makro, jadi sebagai contoh pengujian di perpustakaan ini, Anda biasanya melihat gambar seperti ini:
namespace {
Macro di C ++ waspada, mengapa mereka begitu berkembang di perpustakaan untuk membuat tes?
Pustaka tes unit harus memberi para penggunanya cara untuk menulis tes sehingga runtime tes dapat menemukan dan menjalankannya dengan cara apa pun. Ketika Anda berpikir tentang cara melakukan ini, menggunakan makro tampaknya paling mudah. Makro TEST () biasanya mendefinisikan fungsi (dalam kasus Google Test, makro juga membuat kelas) dan memastikan bahwa alamat fungsi ini masuk ke beberapa wadah global.
Pustaka yang terkenal di mana pendekatan tanpa makro tunggal dilaksanakan adalah kerangka kerja tut . Mari kita lihat contohnya dari tutorial:
#include <tut/tut.hpp> namespace tut { struct basic{}; typedef test_group<basic> factory; typedef factory::object object; } namespace { tut::factory tf("basic test"); } namespace tut { template<> template<> void object::test<1>() { ensure_equals("2+2=?", 2+2, 4); } }
Gagasan yang mendasari cukup menarik dan berhasil, itu tidak terlalu sulit. Singkatnya, Anda memiliki kelas dasar yang mengimplementasikan fungsi template yang melibatkan parameterisasi dengan integer:
template <class Data> class test_object : public Data { template <int n> void test() { called_method_was_a_dummy_test_ = true; } }
Sekarang ketika Anda menulis tes seperti itu:
template<> template<> void object::test<1>() { ensure_equals("2+2=?", 2+2, 4); }
Anda benar-benar membuat spesialisasi metode pengujian untuk angka tertentu N = 1 (inilah persisnya template<>template<>
singkatan). Dengan memanggil test<N>()
runtime tes dapat memahami apakah itu tes yang sebenarnya atau itu adalah rintisan melihat nilai yang called_method_was_a_dummy_test_
setelah tes dieksekusi.
Berikutnya, ketika Anda mendeklarasikan grup uji:
tut::factory tf("basic test");
Pertama, Anda menghitung semua test<N>
ke konstanta tertentu yang ditransfer ke perpustakaan, dan kedua, dengan efek samping, Anda menambahkan informasi tentang grup ke wadah global (nama grup dan alamat semua fungsi tes).
Pengecualian digunakan sebagai kondisi pengujian dalam tut, jadi fungsi tut::ensure_equals()
akan dengan mudah melempar pengecualian jika dua nilai yang diteruskan ke sana tidak sama, dan lingkungan uji coba akan menangkap pengecualian dan menganggap pengujian gagal. Saya suka pendekatan ini, segera menjadi jelas bagi pengembang C ++ mana pun pernyataan seperti itu dapat digunakan. Misalnya, jika pengujian saya membuat utas bantu, maka tidak ada gunanya menempatkan pernyataan di sana, tidak ada yang akan menangkapnya. Selain itu, jelas bagi saya bahwa pengujian saya harus dapat membebaskan sumber daya jika terjadi pengecualian, seolah-olah itu adalah kode aman-pengecualian biasa.
Pada prinsipnya, perpustakaan tut-framework terlihat cukup bagus, tetapi ada beberapa kelemahan dalam implementasinya. Sebagai contoh, untuk kasus saya, saya ingin tes tidak hanya memiliki angka, tetapi juga atribut lainnya, khususnya nama, serta "ukuran" tes (misalnya, apakah itu tes integrasi atau tes unit). Ini dapat dipecahkan dalam kerangka tut API, dan bahkan sesuatu sudah ada, dan sesuatu dapat diterapkan jika Anda menambahkan metode ke pustaka API dan memanggilnya ke badan tes untuk mengatur salah satu parameternya:
template<> template<> void object::test<1>() { set_name("2+2");
Masalah lain adalah bahwa lingkungan uji coba tut tidak tahu apa-apa tentang peristiwa seperti awal tes. Lingkungan mengeksekusi object::test<N>()
dan tidak tahu sebelumnya apakah tes diimplementasikan untuk N yang diberikan, atau itu hanya sebuah rintisan. Dia hanya called_method_was_a_dummy_test_
kapan tes selesai dengan menganalisis nilai yang called_method_was_a_dummy_test_
. Fitur ini tidak menunjukkan dirinya dengan sangat baik dalam sistem CI, yang dapat mengelompokkan output yang dibuat oleh program antara awal dan akhir pengujian.
Namun, menurut pendapat saya, hal utama yang dapat ditingkatkan ("kesalahan fatal") adalah adanya kode tambahan yang diperlukan untuk menulis tes. Ada cukup banyak hal dalam tutorial tut-framework: diusulkan untuk terlebih dahulu membuat kelas struct basic{}
, dan menggambarkan tes sebagai metode objek yang terkait dengan ini. Di kelas ini, Anda dapat menentukan metode dan data yang ingin Anda gunakan dalam kelompok uji, dan konstruktor dan destruktor membingkai pelaksanaan tes, menciptakan hal seperti fixture dari jUnit. Dalam latihan saya dengan tut, objek ini hampir selalu kosong, tetapi ia menyeret sejumlah baris kode tertentu.
Jadi, kami pergi ke bengkel sepeda dan mencoba mengatur idenya dalam bentuk perpustakaan kecil.
Seperti inilah tampilan file tes minimal di perpustakaan yang diuji:
Selain kekurangan makro, bonusnya adalah kurangnya memori dinamis di dalam perpustakaan.
Definisi kasus uji
Untuk pendaftaran tes, sihir tingkat dasar entri digunakan pada prinsip yang sama dengan tut. Di suatu tempat di diuji. Ada fungsi boilerplate semacam ini:
template <int N> static void Case(IRuntime* runtime) { throw TheCaseIsAStub(); }
Kasus uji yang ditulis oleh pengguna perpustakaan hanyalah spesialisasi dari metode ini. Fungsi ini dinyatakan statis, mis. di setiap unit terjemahan, kami membuat spesialisasi yang tidak bersinggungan dengan nama satu sama lain selama penautan.
Ada aturan seperti itu yang pertama kali Anda perlu panggil StartCase()
, di mana Anda bisa lulus hal-hal seperti nama tes dan mungkin beberapa hal lain yang masih dalam pengembangan.
Ketika tes memanggil runtime->StartTest()
, hal-hal menarik dapat terjadi. Pertama, jika tes sekarang dalam mode jalankan, maka Anda dapat memberi tahu suatu tempat bahwa tes telah mulai dieksekusi. Kedua, jika ada mode pengumpulan informasi tentang tes yang tersedia, StartTest()
melempar jenis pengecualian khusus yang akan berarti bahwa tes itu nyata, dan bukan rintisan.
Pendaftaran
Pada titik tertentu, Anda perlu mengumpulkan alamat semua kotak uji dan menyimpannya di suatu tempat. Dalam menguji, ini dilakukan dengan menggunakan kelompok. Konstruktor kelas Grup :: teruji melakukan ini sebagai efek samping:
static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);
Konstruktor membuat grup dengan nama yang ditentukan dan menambahkannya semua Case<N>
yang ditemukan di unit terjemahan saat ini. Ternyata dalam satu unit terjemahan Anda tidak dapat memiliki dua grup. Ini juga berarti bahwa Anda tidak dapat membagi satu grup menjadi beberapa unit terjemahan.
Parameter template adalah berapa banyak kasus uji yang harus dicari dalam unit terjemahan saat ini untuk grup yang dibuat.
Tautan
Dalam contoh di atas, penciptaan objek yang diuji :: Group () terjadi di dalam fungsi yang harus kita panggil dari aplikasi kita untuk mendaftarkan tes:
void LinkStdVectorTests() { static tested::Group<CASE_COUNTER> x("std.vector", __FILE__); }
Fungsi tidak selalu diperlukan, kadang-kadang Anda bisa mendeklarasikan objek dari kelas tested::Group
di dalam file. Namun, pengalaman saya adalah bahwa tautan kadang-kadang "mengoptimalkan" seluruh file jika dirakit di dalam perpustakaan, dan tidak ada aplikasi utama yang menggunakan karakter apa pun dari file cpp ini:
calc.lib <- calc_test.lib(calc_test.cpp) ^ ^ | | app.exe run_test.exe
Ketika calc_test.cpp tidak ditautkan dari sumber run_test.exe, penghubung hanya menghapus file ini dari pertimbangan seluruhnya, bersama dengan penciptaan objek statis, meskipun faktanya ia memiliki efek samping yang kita butuhkan.
Jika rantai mana yang dihasilkan dari run_test.exe, maka objek statis akan muncul di file yang dapat dieksekusi. Dan tidak masalah bagaimana ini dilakukan, seperti dalam contoh:
void LinkStdVectorTests() { static tested::Group<CASE_COUNTER> x("std.vector", __FILE__); }
atau lebih:
static tested::Group<CASE_COUNTER> x("std.vector", __FILE__); void LinkStdVectorTests() { }
Opsi pertama, menurut saya, lebih baik karena konstruktor dipanggil setelah dimulainya main (), dan aplikasi memiliki kontrol atas proses ini.
Saya pikir pengaturan kruk ini diperlukan untuk setiap perpustakaan pengujian unit yang menggunakan variabel global dan efek samping dari konstruktor untuk membuat database pengujian. Namun, itu mungkin dapat dihindari dengan menghubungkan pustaka uji dengan kunci - seluruh arsip (analog di MSVC hanya muncul di Visual Studio 2015.3).
Makro
Saya berjanji bahwa tidak akan ada makro, tapi itu - CASE_COUNTER
. Opsi kerjanya adalah bahwa ia digunakan oleh __COUNTER__
, makro yang ditambahkan oleh kompiler setiap kali digunakan di dalam unit terjemahan.
Didukung oleh GCC, CLANG, MSVC, tetapi tidak standar. Jika ini membuat frustrasi, berikut adalah beberapa alternatif:
- gunakan angka 0, 1, 2
- gunakan
__LINE__
standar. - gunakan sihir constexpr level 80. Anda dapat mencari "penghitung constexpr" dan mencoba menemukan kompiler yang akan berfungsi.
Masalah dengan __LINE__
adalah bahwa menggunakan angka besar dalam opsi templat membuat ukuran file besar yang dapat dieksekusi. Itu sebabnya saya membatasi jenis pola char yang ditandatangani menjadi 128 sebagai jumlah tes maksimum dalam grup.
Kegagalan memori dinamis
Ternyata saat mendaftar tes, Anda tidak dapat menggunakan memori dinamis, yang saya gunakan. Ada kemungkinan bahwa lingkungan Anda tidak memiliki memori dinamis atau Anda menggunakan pencarian untuk kebocoran memori dalam kasus uji, sehingga intervensi dari lingkungan pelaksanaan tes bukan yang Anda butuhkan. Google Test sedang bergumul dengan ini, berikut ini cuplikan dari sana:
Dan kita tidak bisa membuat kesulitan.
Lalu bagaimana kita mendapatkan daftar tes? Ini lebih internal teknis, yang lebih mudah dilihat dalam kode sumber, tetapi saya akan memberitahu Anda.
Saat membuat grup, kelasnya akan menerima pointer ke fungsi tested::CaseCollector<CASE_COUNTER>::collect
, yang akan mengumpulkan semua tes unit terjemahan ke dalam daftar. Begini cara kerjanya:
Ternyata di setiap unit terjemahan banyak variabel statis tipe CaseListEntry CaseCollector \ :: s_caseListEntry dibuat, yang merupakan elemen dari daftar tes, dan metode collect () mengumpulkan elemen-elemen ini dalam daftar yang terhubung secara tunggal. Dengan cara yang kira-kira sama, daftar ini membentuk kelompok-kelompok tes, tetapi tanpa pola dan rekursi.
Struktur
Tes memerlukan pengikatan yang berbeda, seperti output ke konsol dalam huruf merah Gagal, membuat laporan pengujian dalam format yang dapat dimengerti untuk CI atau GUI di mana Anda dapat melihat daftar tes dan menjalankan yang dipilih - secara umum, banyak hal. Saya memiliki visi tentang bagaimana hal ini dapat dilakukan, yang berbeda dari apa yang saya lihat sebelumnya di perpustakaan pengujian. Klaim ini terutama untuk perpustakaan yang menyebut diri mereka "hanya header", sementara termasuk sejumlah besar kode, yang pada dasarnya bukan untuk file header.
Pendekatan yang saya asumsikan adalah bahwa kita membagi perpustakaan menjadi front-end - ini diuji.h dan back-end perpustakaan sendiri. Untuk menulis tes, Anda hanya perlu diuji.h, yang sekarang C ++ 17 (karena std :: std :: string_view) tetapi diasumsikan bahwa akan ada C ++ 98. Tested.h benar-benar melakukan registrasi dan mencari tes, opsi peluncuran yang minimal nyaman, serta kemampuan untuk mengekspor tes (grup, alamat fungsi kasus uji). Pustaka back-end yang belum ada dapat melakukan apa pun yang mereka butuhkan dalam hal menghasilkan hasil dan meluncurkan menggunakan fungsi ekspor. Dengan cara yang sama, Anda dapat menyesuaikan peluncuran dengan kebutuhan proyek Anda.
Ringkasan
Pustaka yang diuji ( kode github ) masih membutuhkan beberapa stabilisasi. Dalam waktu dekat, tambahkan kemampuan untuk menjalankan tes asinkron (diperlukan untuk tes integrasi di WebAssembly) dan tunjukkan ukuran tes. Menurut pendapat saya, perpustakaan masih belum cukup siap untuk penggunaan produksi, tetapi tiba-tiba saya menghabiskan banyak waktu dan panggung telah berhenti, mengambil napas dan meminta umpan balik dari masyarakat. Apakah Anda tertarik untuk menggunakan perpustakaan semacam ini? Mungkin ada ide lain di gudang C ++ karena mungkin untuk membuat perpustakaan tanpa makro? Apakah pernyataan masalah seperti itu menarik sama sekali?