
Halo! Nama saya Yuri Skvortsov , tim kami terlibat dalam pengujian otomatis di Rosbank. Salah satu tugas kami adalah mengembangkan alat untuk mengotomatiskan pengujian fungsional.
Pada artikel ini, saya ingin berbicara tentang solusi yang dipahami sebagai utilitas tambahan kecil untuk memecahkan masalah lain, tetapi pada akhirnya berubah menjadi alat yang berdiri sendiri. Kita berbicara tentang kerangka Fast-Unit, yang memungkinkan Anda menulis pengujian unit dalam gaya deklaratif dan mengubah pengembangan pengujian unit menjadi konstruktor komponen. Proyek ini dikembangkan terutama untuk menguji produk utama kami - Tladianta - kerangka kerja BDD terpadu untuk menguji 4 platform: Desktop, Web, Seluler, dan Lainnya.
Pertama-tama, menguji kerangka kerja otomatisasi bukanlah tugas yang umum. Namun, dalam kasus ini, ini bukan bagian dari proyek pengujian, tetapi produk independen, jadi kami segera menyadari kebutuhan unit.
Pada tahap pertama, kami mencoba menggunakan alat yang sudah jadi seperti assertJ dan Mockito, tetapi dengan cepat kami menemukan beberapa fitur teknis dari proyek kami:
- Tladianta sudah menggunakan JUnit4 sebagai dependensi, yang membuatnya sulit untuk menggunakan versi JUnit yang berbeda dan memperumit Before;
- Tladianta berisi komponen untuk bekerja dengan platform berbeda, ia memiliki banyak entitas yang "sangat dekat" dalam hal fungsionalitas, tetapi dengan hierarki dan perilaku berbeda;
- ยซยป ( ) ;
- , , , , ;
- - (, Appium , , , );
- , : Mockito .
Awalnya, ketika kita baru belajar mengganti driver, membuat elemen Selenium palsu dan menulis arsitektur dasar untuk test harness, pengujiannya terlihat seperti ini:
@Test
public void checkOpenHint() {
ElementManager.getInstance().register(xpath,ElementManager.Condition.VISIBLE,
ElementManager.Condition.DISABLED);
new HintStepDefs().open(("");
assertTrue(TestResults.getInstance().isSuccessful("Open"));
assertTrue(TestResults.getInstance().isSuccessful("Click"));
}
@Test
public void checkCloseHint() {
ElementManager.getInstance().register(xpath);
new HintStepDefs().close("");
assertTrue(TestResults.getInstance().isSuccessful("Close"));
assertTrue(TestResults.getInstance().isSuccessful("Click"));
}
Atau bahkan seperti ini:
@Test
public void fillFieldsTestOld() {
ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX,"//check-box","",
ElementManager.Condition.NOT_SELECTED);
ElementManager.getInstance().register(ElementManager.Type.INPUT,"//input","");
ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP,
"//radio-group","");
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
new HtmlCommonSteps().fillFields(dataTable);
assertEquals(TestResults.getInstance().getTestResult("set"),
ElementProvider.getInstance().provide("//check-box").force().getAttribute("test-id"));
assertEqualsTestResults.getInstance().getTestResult("sendKeys"),
ElementProvider.getInstance().provide("//input").force().getAttribute("test-id"));
assertEquals(TestResults.getInstance().getTestResult("selectByValue"),
ElementProvider.getInstance().provide("//radio-group").force().getAttribute("test-id"));
}
Tidak sulit untuk menemukan apa yang sedang diuji dalam kode di atas, serta untuk memahami pemeriksaan, tetapi ada sejumlah besar kode. Jika Anda menyertakan perangkat lunak untuk memeriksa dan menjelaskan kesalahan, maka itu menjadi sangat sulit untuk dibaca. Dan kami hanya mencoba untuk memeriksa bahwa metode tersebut dipanggil pada objek yang diinginkan, sedangkan logika pemeriksaan yang sebenarnya sangat primitif. Untuk menulis pengujian seperti itu, Anda perlu mengetahui tentang ElementManager, ElementProvider, TestResults, TickingFuture (pembungkus untuk mengimplementasikan perubahan status elemen selama waktu tertentu). Komponen ini berbeda dalam proyek yang berbeda, kami tidak punya waktu untuk menyinkronkan perubahan.
Tantangan lainnya adalah pengembangan beberapa standar. Tim kami memiliki kelebihan dari automators, banyak dari kami tidak memiliki cukup pengalaman dalam mengembangkan pengujian unit, dan meskipun, pada pandangan pertama, itu sederhana, membaca kode satu sama lain cukup melelahkan. Kami mencoba melikuidasi utang teknis dengan cukup cepat, dan ketika ratusan pengujian semacam itu muncul, menjadi sulit untuk dipertahankan. Selain itu, kode ternyata kelebihan beban dengan konfigurasi, pemeriksaan nyata hilang, dan tali tebal mengarah pada fakta bahwa alih-alih menguji fungsionalitas kerangka kerja, tali kami sendiri diuji.
Dan ketika kami mencoba untuk mentransfer perkembangan dari satu modul ke modul lainnya, menjadi jelas bahwa kami perlu mengeluarkan fungsionalitas umum. Pada saat itu, lahir ide tidak hanya untuk membuat perpustakaan dengan praktik terbaik, tetapi juga untuk membuat proses pengembangan unit tunggal dalam alat ini.
Mengubah filosofi
Jika Anda melihat kode secara keseluruhan, Anda dapat melihat bahwa banyak blok kode diulang "tanpa arti". Kami menguji metode, tetapi kami menggunakan konstruktor sepanjang waktu (untuk menghindari kemungkinan beberapa kesalahan di-cache). Transformasi pertama - kami memindahkan validasi dan pembuatan instance yang diuji ke dalam anotasi.
@IExpectTestResult(errDesc = " set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " sendKeys", value = "sendKeys",
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@Test
public void fillFieldsTestOld() {
ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX, "//check-box", "",
ElementManager.Condition.NOT_SELECTED);
ElementManager.getInstance().register(ElementManager.Type.INPUT, "//input", "");
ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP,
"//radio-group", "");
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
runTest("fillFields", dataTable);
}
Apa yang berubah?
- Pemeriksaan telah didelegasikan ke komponen terpisah. Sekarang Anda tidak perlu tahu tentang bagaimana item disimpan, hasil tes.
- : errDesc , .
- , , , โ runTest, , .
- .
- - , .
Kami menyukai bentuk notasi ini, dan kami memutuskan untuk menyederhanakan komponen kompleks lainnya dengan cara yang sama - pembuatan elemen. Sebagian besar pengujian kami dikhususkan untuk langkah-langkah yang sudah jadi, dan kami harus memastikan bahwa langkah-langkah tersebut bekerja dengan benar, namun, untuk pemeriksaan seperti itu, aplikasi palsu harus sepenuhnya "diluncurkan" dan mengisinya dengan elemen (ingat bahwa kita berbicara tentang Web, Desktop, dan Seluler, alat yang sangat berbeda).
@IGenerateElement(type = ElementManager.Type.CHECK_BOX)
@IGenerateElement(type = ElementManager.Type.RADIO_GROUP)
@IGenerateElement(type = ElementManager.Type.INPUT)
@Test
@IExpectTestResult(errDesc = " set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " sendKeys", value = "sendKeys",
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
public void fillFieldsTest() {
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
runTest("fillFields", dataTable);
}
Sekarang kode uji telah menjadi template lengkap, parameternya terlihat jelas, dan semua logika dipindahkan ke komponen template. Properti default memungkinkan untuk menghapus baris kosong dan memberikan banyak peluang untuk kelebihan muatan. Kode ini hampir sejalan dengan pendekatan BDD, precondition, validation, action. Selain itu, semua pengikatan telah lepas dari logika pengujian, Anda tidak perlu lagi mengetahui tentang manajer, penyimpanan hasil pengujian, kodenya sederhana dan mudah dibaca. Karena anotasi di Java hampir tidak dapat disesuaikan, kami memperkenalkan mekanisme untuk konverter yang dapat menerima hasil akhir dari sebuah string. Kode ini tidak hanya memeriksa fakta pemanggilan metode, tetapi juga id dari elemen yang mengeksekusinya. Hampir semua pengujian yang ada pada waktu itu (lebih dari 200 unit) dengan cepat ditransfer ke logika ini, membawanya ke satu template. Tes telah menjadi apa yang seharusnya - dokumentasi,bukan kode, jadi kami sampai pada deklaratif. Pendekatan inilah yang membentuk dasar Fast-Unit - deklaratif, tes dokumentasi mandiri dan isolasi fungsionalitas yang diuji, tes ini sepenuhnya dikhususkan untuk memeriksa satu metode pengujian.
Kami terus berkembang
Sekarang perlu menambahkan kemampuan untuk membuat komponen tersebut secara mandiri dalam kerangka proyek, menambahkan kemampuan untuk mengontrol urutan operasinya. Untuk melakukan ini, kami mengembangkan konsep fase: tidak seperti Junit, semua fase ini ada secara independen dalam setiap pengujian dan dijalankan pada saat pengujian dijalankan. Sebagai implementasi default, kami telah menetapkan siklus hidup berikut:
- Package-generate - memproses anotasi yang terkait dengan info-paket. Komponen yang terkait dengannya menyediakan unduhan konfigurasi dan persiapan harness umum.
- Class-generate - memproses anotasi yang terkait dengan kelas pengujian. Tindakan konfigurasi yang terkait dengan kerangka kerja dilakukan di sini, menyesuaikannya dengan pengikatan yang telah disiapkan.
- Hasilkan - memproses anotasi yang terkait dengan metode pengujian itu sendiri (titik masuk).
- Uji - menyiapkan sebuah instance dan menjalankan metode yang diuji.
- Tegaskan - melakukan pemeriksaan.
Anotasi yang akan diproses dijelaskan seperti ini:
@Target(ElementType.PACKAGE) //
@IPhase(value = "package-generate", processingClass = IStabDriver.StabDriverProcessor.class,
priority = 1) // ( )
public @interface IStabDriver {
Class<? extends WebDriver> value(); // ,
class StabDriverProcessor implements PhaseProcessor<IStabDriver> { //
@Override
public void process(IStabDriver iStabDriver) {
//
}
}
}
Fitur Fast-Unit adalah bahwa siklus hidup dapat diganti untuk semua kelas - ini dijelaskan oleh anotasi ITestClass, yang dirancang untuk menunjukkan kelas dan fase yang diuji. Daftar fase ditentukan hanya sebagai larik string, memungkinkan perubahan komposisi dan urutan fase. Metode yang menangani fase juga ditemukan menggunakan anotasi, sehingga dimungkinkan untuk membuat penangan yang diperlukan di kelas Anda dan menandainya (plus, menimpa dalam kelas tersedia). Nilai tambah yang besar adalah bahwa pemisahan ini memungkinkan kami untuk membagi pengujian menjadi beberapa lapisan: jika kesalahan dalam pengujian yang telah selesai terjadi selama fase pembuatan atau pembuatan paket, maka harness pengujian rusak. Jika class-generate - ada masalah dalam mekanisme konfigurasi kerangka kerja. Jika dalam kerangka pengujian ada kesalahan dalam fungsionalitas yang diuji.Fase pengujian secara teknis dapat menampilkan kesalahan baik dalam pengikatan maupun dalam fungsionalitas yang diuji, jadi kami menggabungkan kemungkinan kesalahan pengikatan dalam tipe khusus - InnerException.
Setiap fase diisolasi, mis. tidak bergantung pada dan tidak berinteraksi langsung dengan fase lain, satu-satunya hal yang dilewati antar fase adalah kesalahan (kebanyakan fase akan dilewati jika terjadi kesalahan pada fase sebelumnya, tetapi ini tidak diperlukan, misalnya fase menegaskan akan tetap berfungsi).
Di sini, mungkin, pertanyaan telah muncul, dari mana contoh pengujian itu berasal. Jika konstruktornya kosong, sudah jelas: menggunakan Reflection API, Anda cukup membuat instance kelas yang diuji. Tetapi bagaimana Anda meneruskan parameter dalam konstruksi ini atau mengonfigurasi instance setelah konstruktor diaktifkan? Apa yang harus dilakukan jika objek sedang dibuat oleh pembuat atau tentang pengujian statis? Untuk ini, mekanisme penyedia telah dikembangkan, yang menyembunyikan kompleksitas konstruktor.
Parameterisasi default:
@IProvideInstance
CheckBox generateCheckBox() {
return new CheckBox((MobileElement) ElementProvider.getInstance().provide("//check-box")
.get());
}
Tidak ada parameter - tidak masalah (kami menguji kelas CheckBox dan mendaftarkan metode yang akan membuat instance untuk kami). Karena penyedia default diganti di sini, tidak perlu menambahkan apa pun dalam pengujian itu sendiri, mereka akan secara otomatis menggunakan metode ini sebagai sumber. Contoh ini dengan jelas mengilustrasikan logika Fast-Unit - kami menyembunyikan kompleks dan tidak perlu. Dari sudut pandang pengujian, tidak masalah sama sekali bagaimana dan dari mana elemen seluler yang dibungkus dengan kelas Kotak Centang berasal. Yang penting bagi kami adalah bahwa ada beberapa objek Kotak Centang yang memenuhi persyaratan yang ditentukan.
Injeksi argumen otomatis: mari kita asumsikan kita memiliki konstruktor seperti ini:
public Mask(String dataFormat, String fieldFormat) {
this.dataFormat = dataFormat;
this.fieldFormat = fieldFormat;
}
Kemudian tes kelas ini menggunakan injeksi argumen akan terlihat seperti ini:
Object[] dataMask={"_:2_:2_:4","_:2/_:2/_:4"};
@ITestInstance(argSource = "dataMask")
@Test
@IExpectTestResult(errDesc = " ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
runTest("convert","12102012");
}
Penyedia bernama
Terakhir, jika beberapa penyedia diperlukan, kami menggunakan pengikatan nama, tidak hanya menyembunyikan kompleksitas konstruktor, tetapi juga mengungkapkan arti sebenarnya. Masalah yang sama bisa diselesaikan seperti ini:
@IProvideInstance("")
Mask createDataMask(){
return new Mask("_:2_:2_:4","_:2/_:2/_:4");
}
@ITestInstance("")
@Test
@IExpectTestResult(errDesc = " ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
runTest("convert","12102012");
}
IProvideInstance dan ITestInstance adalah anotasi terkait yang memungkinkan Anda memberi tahu metode tempat mendapatkan instans yang sedang diuji (untuk statika, ia hanya mengembalikan nol, karena instans ini pada akhirnya digunakan melalui API Refleksi). Pendekatan penyedia memberikan lebih banyak informasi tentang apa yang sebenarnya terjadi dalam pengujian, mengganti panggilan ke konstruktor dengan beberapa parameter dengan teks yang menjelaskan prasyarat, jadi jika konstruktor tiba-tiba berubah, kita hanya perlu mengoreksi penyedia, tetapi pengujian tidak akan berubah sampai fungsionalitas sebenarnya berubah. Jika, selama peninjauan, Anda melihat beberapa penyedia, Anda akan memperhatikan perbedaan di antara mereka, dan oleh karena itu, kekhasan perilaku metode yang diuji. Bahkan tanpa mengetahui kerangka kerja sama sekali, tetapi hanya mengetahui prinsip-prinsip operasi Unit Cepat,pengembang akan dapat membaca kode pengujian dan memahami apa yang dilakukan metode yang diuji.
Kesimpulan dan hasil
Pendekatan kami ternyata memiliki banyak keuntungan:
- Portabilitas uji yang mudah.
- Menyembunyikan kompleksitas binding, kemungkinan refactoringnya tanpa merusak pengujian.
- Kompatibilitas mundur dijamin - perubahan nama metode akan dicatat sebagai kesalahan.
- Tes telah berubah menjadi dokumentasi yang cukup rinci untuk setiap metode.
- Kualitas inspeksi meningkat secara signifikan.
- Pengembangan unit test telah menjadi proses pipeline, dan kecepatan pengembangan dan review telah meningkat secara signifikan.
- Stabilitas tes yang dikembangkan - meskipun kerangka kerja dan Fast-Unit sendiri berkembang secara aktif, tidak ada penurunan tes
Terlepas dari kerumitan yang tampak, kami dapat dengan cepat mengimplementasikan alat ini. Sekarang sebagian besar unit ditulis di dalamnya, dan mereka telah mengkonfirmasi keandalannya dengan migrasi yang cukup kompleks dan banyak, mereka dapat mengidentifikasi cacat yang agak rumit (misalnya, menunggu elemen dan pemeriksaan teks). Kami dapat dengan cepat menghilangkan utang teknis dan membangun kerja sama yang efektif dengan unit, menjadikannya bagian integral dari pengembangan. Sekarang kami sedang mempertimbangkan opsi untuk implementasi yang lebih aktif dari alat ini di proyek lain di luar tim kami.
Masalah dan rencana saat ini:
- , . , ( - ).
- .
- .
- , -.
- Fast-Unit junit4, junit5 testng