Pengembangan tes visual berdasarkan Gemini dan Storybook

Halo, Habr! Dalam artikel ini saya ingin berbagi pengalaman mengembangkan tes visual di tim kami.

Kebetulan kami tidak langsung berpikir tentang pengujian tata letak. Nah, beberapa frame akan bergerak beberapa piksel, ya, perbaiki. Pada akhirnya, ada penguji - lalat tidak akan terbang melewati mereka. Tetapi faktor manusia masih tidak bisa dibohongi - untuk mendeteksi perubahan kecil pada antarmuka pengguna jauh dari selalu mungkin secara fisik bahkan untuk tester. Pertanyaan muncul ketika optimalisasi tata letak dan transisi ke BEM dimulai. Di sini, itu pasti tidak akan rugi, dan kami sangat membutuhkan cara otomatis untuk mendeteksi situasi ketika, sebagai hasil edit, sesuatu di UI mulai berubah tidak seperti yang dimaksudkan, atau tidak di tempat yang dimaksudkan.

Setiap pengembang tahu tentang pengujian kode unit. Tes unit memberikan keyakinan bahwa perubahan kode tidak merusak apa pun. Yah, setidaknya mereka tidak mematahkan bagian yang ada tes. Prinsip yang sama dapat diterapkan pada antarmuka pengguna. Sama seperti tes unit test kelas, tes visual menguji komponen visual yang membentuk antarmuka pengguna suatu aplikasi.

Untuk komponen visual, Anda dapat menulis tes unit "klasik", yang, misalnya, memulai rendering komponen dengan nilai parameter input yang berbeda dan memeriksa keadaan yang diharapkan dari pohon DOM menggunakan pernyataan tegas, membandingkan elemen individual atau potret pohon DOM komponen dengan referensi secara umum. Tes visual juga didasarkan pada foto, tetapi sudah pada foto tampilan visual komponen (tangkapan layar). Inti dari tes visual adalah untuk membandingkan gambar yang diambil selama tes dengan yang referensi dan, jika perbedaan ditemukan, baik menerima gambar baru sebagai referensi atau memperbaiki bug yang menyebabkan perbedaan ini.

Tentu saja, "menyaring" komponen visual individual tidak terlalu efektif. Komponen tidak hidup dalam ruang hampa dan tampilannya mungkin tergantung pada komponen tingkat atas atau yang berdekatan. Tidak masalah bagaimana kita menguji komponen individu, gambar secara keseluruhan mungkin memiliki cacat. Di sisi lain, jika Anda mengambil gambar dari seluruh jendela aplikasi, maka banyak gambar akan berisi komponen yang sama, yang berarti bahwa jika Anda mengubah satu komponen, kami akan dipaksa untuk memperbarui semua gambar di mana komponen ini ada.

Kebenarannya, seperti biasa, ada di suatu tempat di tengah - Anda dapat menggambar seluruh halaman aplikasi, tetapi mengambil gambar hanya satu area di mana tes dibuat, dalam kasus tertentu area ini mungkin bertepatan dengan area komponen tertentu, tetapi ini tidak akan menjadi komponen dalam kekosongan, tetapi dalam lingkungan yang sangat nyata. Dan ini sudah akan mirip dengan uji visual unit, meskipun kita tidak dapat berbicara tentang modularitas jika "unit" tahu sesuatu tentang lingkungan. Baiklah, oke, tidak terlalu penting apakah kategori tes termasuk tes visual - modular atau integrasi. Seperti kata pepatah, "Anda memeriksa atau pergi?"

Pemilihan alat


Untuk mempercepat pelaksanaan tes, rendering halaman dapat dilakukan di beberapa browser tanpa kepala yang melakukan semua pekerjaan di memori tanpa ditampilkan di layar dan memastikan kinerja maksimum. Tetapi dalam kasus kami, sangat penting untuk memastikan bahwa aplikasi berfungsi di Internet Explorer (IE), yang tidak memiliki mode tanpa kepala, dan kami membutuhkan alat untuk mengelola browser secara terprogram. Untungnya, semuanya telah ditemukan sebelum kita dan ada instrumen seperti itu - disebut Selenium . Sebagai bagian dari proyek Selenium, driver sedang dikembangkan untuk mengelola berbagai browser, termasuk driver untuk IE. Server selenium dapat mengelola browser tidak hanya secara lokal, tetapi juga dari jarak jauh, membentuk sekelompok server selenium, yang disebut jaringan selenium.

Selenium adalah alat yang ampuh, tetapi ambang untuk memasukkannya cukup tinggi. Kami memutuskan untuk mencari alat yang siap pakai untuk pengujian visual berdasarkan Selenium dan menemukan produk hebat dari Yandex bernama Gemini . Gemini dapat mengambil gambar, termasuk gambar dari area halaman tertentu, membandingkan gambar dengan yang referensi, memvisualisasikan perbedaan dan memperhitungkan momen-momen seperti anti-aliasing atau kursor yang berkedip. Selain itu, Gemini dapat melakukan tayangan ulang tes jatuh, memparalelkan pelaksanaan tes, dan banyak barang lainnya. Secara umum, kami memutuskan untuk mencoba.

Tes Gemini mudah ditulis. Pertama, Anda perlu menyiapkan infrastruktur - instal selenium-standalone dan mulai server selenium. Kemudian konfigurasikan gemini, tentukan alamat aplikasi yang sedang diuji (rootUrl), alamat server selenium (gridUrl), komposisi dan konfigurasi browser, serta plugin yang diperlukan untuk menghasilkan laporan, mengoptimalkan kompresi gambar. Contoh Konfigurasi:

//.gemini.js module.exports = { rootUrl: 'http://my-app.ru', gridUrl: 'http://127.0.0.1:4444/wd/hub', browsers: { chrome: { windowSize: '1920x1080', screenshotsDir:'gemini/screens/1920x1080' desiredCapabilities: { browserName: 'chrome' } } }, system: { projectRoot: '', plugins: { 'html-reporter/gemini': { enabled: true, path: './report' }, 'gemini-optipng': true }, exclude: [ '**/report/*' ], diffColor: '#EC041E' } }; 

Tes itu sendiri adalah kumpulan suite, di mana masing-masing satu atau lebih gambar (negara bagian) diambil. Sebelum mengambil metode snapshot (tangkap ()), Anda dapat mengatur area halaman yang akan diambil menggunakan metode setCaptureElements (), dan juga melakukan beberapa tindakan persiapan jika perlu dalam konteks browser menggunakan metode objek tindakan atau menggunakan kode JavaScript acak - untuk ini dalam tindakan memiliki metode executeJS ().

Contoh:

 gemini.suite('login-dialog', suite => { suite.setUrl('/') .setCaptureElements('.login__form') .capture('default'); .capture('focused', actions => actions.focus('.login__editor')); }); 

Uji data


Alat uji dipilih, tetapi masih jauh dari solusi akhir. Itu perlu untuk memahami apa yang harus dilakukan dengan data yang ditampilkan dalam gambar. Biarkan saya mengingatkan Anda bahwa dalam tes kami memutuskan untuk tidak menggambar komponen individu, tetapi seluruh halaman aplikasi, untuk menguji komponen visual tidak dalam ruang hampa, tetapi dalam lingkungan nyata komponen lainnya. Jika Anda perlu mentransfer data uji yang diperlukan ke alat peraga (saya berbicara tentang komponen reaksi) untuk membuat komponen individual, lebih banyak yang diperlukan untuk membuat seluruh halaman aplikasi, dan mempersiapkan lingkungan untuk pengujian semacam itu bisa menjadi sakit kepala.

Tentu saja, Anda dapat meninggalkan aplikasi itu sendiri untuk menerima data sehingga selama pengujian itu akan mengeksekusi permintaan ke backend, yang, pada gilirannya, akan menerima data dari beberapa jenis database referensi, tetapi bagaimana dengan versi? Anda tidak bisa meletakkan basis data di repositori git. Tidak, tentu saja Anda bisa, tetapi ada beberapa kesopanan.

Atau, untuk menjalankan tes, Anda dapat mengganti server backend nyata dengan yang palsu, yang akan memberikan aplikasi web bukan data dari database, tetapi data statis yang disimpan, misalnya, dalam format json, sudah dengan sumber. Namun, persiapan data tersebut juga tidak terlalu sepele. Kami memutuskan untuk pergi dengan cara yang lebih mudah - bukan untuk menarik data dari server, tetapi hanya untuk mengembalikan keadaan aplikasi (dalam kasus kami, keadaan penyimpanan redux ), yang ada dalam aplikasi pada saat gambar referensi diambil, sebelum menjalankan tes.

Untuk membuat cerita bersambung keadaan saat ini dari toko redux, metode snapshot () telah ditambahkan ke objek jendela:

 export const snapshotStore = (store: Object, fileName: string): string => { let state = store.getState(); const file = new Blob( [ JSON.stringify(state, null, 2) ], { type: 'application/json' } ); let a = document.createElement('a'); a.href = URL.createObjectURL(file); a.download = `${fileName}.testdata.json`; a.click(); return `State downloaded to ${a.download}`; }; const store = createStore(reducer); if (process.env.NODE_ENV !== 'production') { window.snapshot = fileName => snapshotStore(store, fileName); }; 

Menggunakan metode ini, menggunakan baris perintah konsol browser, Anda dapat menyimpan kondisi penyimpanan redux saat ini ke file:

gambar

Sebagai infrastruktur untuk tes visual, Storybook dipilih - alat untuk pengembangan interaktif perpustakaan komponen visual. Gagasan utamanya adalah alih-alih berbagai status komponen di pohon cerita, perbaiki berbagai status aplikasi kita dan gunakan status ini untuk mengambil tangkapan layar. Pada akhirnya, tidak ada perbedaan mendasar antara komponen sederhana dan kompleks, kecuali dalam persiapan lingkungan.

Jadi, setiap tes visual adalah sebuah cerita, sebelum rendering, kondisi penyimpanan redux yang sebelumnya disimpan dalam file dipulihkan. Ini dilakukan dengan menggunakan komponen Penyedia dari pustaka reaksi-redux, ke properti toko yang status deserialisasinya dikembalikan dari file yang disimpan sebelumnya dilewatkan:

 import preloadedState from './incoming-letter.testdata'; const store = createStore(rootReducer, preloadedState); storiesOf('regression/Cards', module) .add('IncomingLetter', () => { return ( <Provider store={store}> <MemoryRouter> <ContextContainer {...dummyProps}/> </MemoryRouter> </Provider> ); }); 

Pada contoh di atas, ContextContainer adalah komponen yang mencakup "kerangka" aplikasi - pohon navigasi, header, dan area konten. Di area konten, berbagai komponen dapat ditampilkan (daftar, kartu, dialog, dll.) Tergantung pada kondisi penyimpanan redux saat ini. Agar komponen tidak memenuhi permintaan yang tidak perlu ke backend untuk input, properti rintisan yang sesuai dilewatkan ke sana.

Dalam konteks Buku Cerita, itu terlihat seperti ini:

gambar

Gemini + buku cerita


Jadi, kami menemukan data untuk tes. Tugas selanjutnya adalah berteman dengan Gemini dan Storybook. Sekilas, semuanya sederhana - dalam konfigurasi Gemini kami menentukan alamat aplikasi yang sedang diuji. Dalam kasus kami, ini adalah alamat server Storybook. Anda hanya perlu menaikkan server buku cerita sebelum memulai tes gemini. Anda dapat melakukan ini langsung dari kode menggunakan berlangganan acara Gemini START_RUNNER dan END_RUNNER:

 const port = 6006; const cofiguration = { rootUrl:`localhost:${port}`, gridUrl: seleniumGridHubUrl, browsers: { 'chrome': { screenshotsDir:'gemini/screens', desiredCapabilities: chromeCapabilities } } }; const Gemini = require('gemini'); const HttpServer = require('http-server'); const runner = new Gemini(cofiguration); const server = HttpServer.createServer({ root: './storybook-static'}); runner.on(runner.events.START_RUNNER, () => { console.log(`storybook server is listening on ${port}...`); server.listen(port); }); runner.on(runner.events.END_RUNNER, () => { server.close(); console.log('storybook server is closed'); }); runner .readTests(path) .done(tests => runner.test(tests)); 

Sebagai server untuk pengujian, kami menggunakan http-server, yang mengembalikan konten folder dengan buku cerita rakitan statis (untuk membangun buku cerita statis, gunakan perintah build-storybook ).

Sejauh ini, semuanya berjalan lancar, tetapi masalah belum membuat mereka menunggu. Faktanya adalah bahwa buku cerita menampilkan cerita di dalam bingkai. Awalnya, kami ingin dapat mengatur wilayah selektif dari gambar menggunakan setCaptureElements (), tetapi ini hanya dapat dilakukan jika Anda menentukan alamat bingkai sebagai alamat untuk suite, sesuatu seperti ini:

 gemini.suite('VisualRegression', suite => suite.setUrl('http://localhost:6006/iframe.html?selectedKind=regression%2Fcards&selectedStory=IncomingLetter') .setCaptureElements('.some-component') .capture('IncomingLetter') ); 

Tapi kemudian ternyata untuk setiap shot kita harus membuat suite sendiri, karena URL dapat ditetapkan untuk suite secara keseluruhan, tetapi tidak untuk snapshot tunggal di dalam suite. Harus dipahami bahwa setiap suite berjalan dalam sesi browser yang terpisah. Ini, pada prinsipnya, benar - tes tidak boleh saling bergantung, tetapi membuka sesi browser terpisah dan memuat Storybook selanjutnya membutuhkan waktu yang cukup banyak, lebih dari sekadar menelusuri cerita dalam kerangka Storybook yang sudah terbuka. Oleh karena itu, dengan sejumlah besar suite, waktu pelaksanaan tes sangat lambat. Bagian dari masalah dapat diselesaikan dengan memparalelkan pelaksanaan tes, tetapi paralelisasi menghabiskan banyak sumber daya (memori, prosesor). Oleh karena itu, setelah memutuskan untuk menghemat sumber daya dan pada saat yang sama tidak kehilangan terlalu banyak dalam durasi uji coba, kami menolak untuk membuka bingkai di jendela browser yang terpisah. Tes dilakukan dalam satu sesi browser, tetapi sebelum setiap pengambilan gambar, cerita selanjutnya dimuat ke dalam bingkai seolah-olah kita baru saja membuka buku cerita dan mengklik pada masing-masing node di pohon cerita. Area gambar - seluruh bingkai:

 gemini.suite('VisualRegression', suite => suite.setUrl('/') .setCaptureElements('#storybook-preview-iframe') .capture('IncomingLetter', actions => openStory(actions, 'IncomingLetter')) .capture('ProjectDocument', actions => openStory(actions, 'ProjectDocumentAccess')) .capture('RelatedDocuments', actions => { openStory(actions, 'RelatedDocuments'); hover(actions, '.related-documents-tree-item__title', 4); }) ); 

Sayangnya, dalam opsi ini, selain kemampuan untuk memilih area gambar, kami juga kehilangan kemampuan untuk menggunakan tindakan standar dari mesin Gemini untuk bekerja dengan elemen-elemen dari pohon DOM (mouseDown (), mouseMove (), mouseMove (), fokus (), dll), dll. untuk. Elemen-elemen dalam bingkai Gemini tidak "melihat." Tetapi kami masih memiliki kesempatan untuk menggunakan fungsi executeJS (), yang dengannya Anda dapat mengeksekusi kode JavaScript dalam konteks browser. Berdasarkan fungsi ini, kami mengimplementasikan analog dari tindakan standar yang kami butuhkan, yang sudah bekerja dalam konteks bingkai Storybook. Di sini kami harus "menyulap" sedikit untuk mentransfer nilai parameter dari konteks pengujian ke konteks browser - mengeksekusi JS (), sayangnya, tidak memberikan kesempatan seperti itu. Oleh karena itu, pada pandangan pertama, kode tampak sedikit aneh - fungsi diterjemahkan ke dalam string, bagian dari kode diganti dengan nilai parameter, dan dalam ExecuteJs () fungsi dikembalikan dari string menggunakan eval ():

 function openStory(actions, storyName) { const storyNameLowered = storyName.toLowerCase(); const clickTo = function(window) { Array.from(window.document.querySelectorAll('a')).filter( function(el) { return el.textContent.toLowerCase() === 'storyNameLowered'; })[0].click(); }; actions.executeJS(eval(`(${clickTo.toString().replace('storyNameLowered', storyNameLowered)})`)); } function dispatchEvents(actions, targets, index, events) { const dispatch = function(window) { const document = window.document.querySelector('#storybook-preview-iframe').contentWindow.document; const target = document.querySelectorAll('targets')[index || 0]; events.forEach(function(event) { const clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent(event, true, true); target.dispatchEvent(clickEvent); }); }; actions.executeJS(eval(`(${dispatch.toString() .replace('targets', targets) .replace('index', index) .replace('events', `["${events.join('","')}"]`)})` )); } function hover(actions, selectors, index) { dispatchEvents(actions, selectors, index, [ 'mouseenter', 'mouseover' ]); } module.exports = { openStory: openStory, hover: hover }; 

Pengulangan eksekusi


Setelah tes visual ditulis dan mulai bekerja, ternyata beberapa tes tidak terlalu stabil. Di suatu tempat, ikon tidak akan punya waktu untuk menggambar, di suatu tempat pemilihan tidak akan dihapus dan kami mendapatkan ketidakcocokan dengan gambar referensi. Oleh karena itu, diputuskan untuk memasukkan pengujian ulang pelaksanaan tes. Namun, di Gemini, coba ulangi pekerjaan untuk seluruh rangkaian, dan seperti yang disebutkan di atas, kami mencoba menghindari situasi di mana rangkaian dibuat untuk setiap tembakan - ini memperlambat pelaksanaan tes terlalu banyak. Di sisi lain, semakin banyak bidikan yang diambil dalam kerangka satu suite, semakin besar kemungkinan eksekusi berulang suite dapat jatuh serta yang sebelumnya. Oleh karena itu, perlu dilakukan retries. Dalam skema kami, pengulangan eksekusi tidak dilakukan untuk seluruh rangkaian, tetapi hanya untuk gambar-gambar yang tidak lulus pada kegagalan sebelumnya. Untuk melakukan ini, di pengendali event TEST_RESULT, kami menganalisis hasil membandingkan snapshot dengan referensi, dan untuk snapshot yang tidak lulus perbandingan, dan hanya untuk mereka, kami membuat suite baru:

 const SuiteCollection = require('gemini/lib/suite-collection'); const Suite = require('gemini/lib/suite'); let retrySuiteCollection; let retryCount = 2; runner.on(runner.events.BEGIN, () => { retrySuiteCollection = new SuiteCollection(); }); runner.on(runner.events.TEST_RESULT, args => { const testId = `${args.state.name}/${args.suite.name}/${args.browserId}`; if (!args.equal) { if (retryCount > 0) console.log(chalk.yellow(`failed ${testId}`)); else console.log(chalk.red(`failed ${testId}`)); let suite = retrySuiteCollection.topLevelSuites().find(s => s.name === args.suite.name); if (!suite) { suite = new Suite(args.suite.name); suite.url = args.suite.url; suite.file = args.suite.file; suite.path = args.suite.path; suite.captureSelectors = [ ...args.suite.captureSelectors ]; suite.browsers = [ ...args.suite.browsers ]; suite.skipped = [ ...args.suite.skipped ]; suite.beforeActions = [ ...args.suite.beforeActions ]; retrySuiteCollection.add(suite); } if (!suite.states.find(s => s.name === args.state.name)) { suite.addState(args.state.clone()); } } else console.log(chalk.green(`passed ${testId}`)); }); 

Omong-omong, acara TEST_RESULT juga berguna untuk memvisualisasikan kemajuan tes saat mereka lulus. Sekarang pengembang tidak perlu menunggu sampai semua tes selesai, ia dapat menghentikan eksekusi jika ia melihat ada sesuatu yang salah. Jika eksekusi pengujian terputus, Gemini akan menutup sesi browser dengan benar yang dibuka oleh server selenium.

Setelah menyelesaikan uji coba, jika suite baru tidak kosong, jalankan uji coba hingga jumlah pengulangan maksimum habis:

 function onComplete(result) { if ((retryCount--) > 0 && result.failed > 0 && retrySuiteCollection.topLevelSuites().length > 0) { runner.test(retrySuiteCollection, {}).done(onComplete); } } runner.readTests(path).done(tests => runner.test(tests).done(onComplete)); 

Ringkasan


Hari ini kami memiliki sekitar lima puluh tes visual yang mencakup keadaan visual utama aplikasi kami. Tentu saja, tidak perlu berbicara tentang cakupan penuh tes UI, tetapi kami belum menetapkan tujuan seperti itu. Tes bekerja dengan sukses baik di workstation pengembang dan di build agent. Sementara pengujian dilakukan hanya dalam konteks Chrome dan Internet Explorer, tetapi di masa depan dimungkinkan untuk menghubungkan browser lain. Semua ekonomi ini melayani jaringan Selemium dengan dua node yang digunakan pada mesin virtual.

Dari waktu ke waktu, kita dihadapkan dengan fakta bahwa setelah rilis versi baru Chrome, perlu memperbarui gambar referensi karena fakta bahwa beberapa elemen mulai ditampilkan sedikit berbeda (misalnya, gulir), tetapi tidak ada yang bisa dilakukan mengenai hal itu. Ini jarang terjadi, tetapi ketika Anda mengubah struktur toko redux, Anda harus mengambil kembali status yang disimpan untuk pengujian. Untuk mengembalikan keadaan yang persis sama dengan yang di tes pada saat penciptaannya, tentu saja, tidak mudah. Sebagai aturan, tidak ada yang sudah mengingat di database mana foto-foto ini diambil dan Anda harus mengambil gambar baru pada data lain. Ini masalah, tapi bukan masalah besar. Untuk mengatasinya, Anda dapat mengambil gambar dengan basis demo, karena kami memiliki skrip untuk generasinya dan selalu diperbarui.

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


All Articles