Apa strategi terbaik untuk aplikasi berbasis database pengujian unit?

346

Saya bekerja dengan banyak aplikasi web yang digerakkan oleh basis data dengan berbagai kompleksitas di backend. Biasanya, ada lapisan ORM yang terpisah dari logika bisnis dan presentasi. Ini membuat unit-test logika bisnis cukup mudah; hal-hal dapat diimplementasikan dalam modul diskrit dan data apa pun yang diperlukan untuk pengujian dapat dipalsukan melalui objek yang mengejek.

Tetapi menguji ORM dan database itu sendiri selalu penuh dengan masalah dan kompromi.

Selama bertahun-tahun, saya telah mencoba beberapa strategi, tidak ada yang benar-benar memuaskan saya.

  • Memuat database uji dengan data yang diketahui. Jalankan tes terhadap ORM dan konfirmasikan bahwa data yang tepat kembali. Kerugiannya di sini adalah bahwa DB pengujian Anda harus mengikuti perubahan skema dalam database aplikasi, dan mungkin tidak sinkron. Itu juga bergantung pada data buatan, dan mungkin tidak mengekspos bug yang terjadi karena input pengguna yang bodoh. Akhirnya, jika database pengujian kecil, itu tidak akan mengungkapkan inefisiensi seperti indeks yang hilang. (Oke, yang terakhir itu sebenarnya bukan unit pengujian yang seharusnya digunakan, tetapi tidak sakit.)

  • Muat salinan database produksi dan uji terhadap itu. Masalahnya di sini adalah bahwa Anda mungkin tidak tahu apa yang ada di DB produksi pada waktu tertentu; tes Anda mungkin perlu ditulis ulang jika data berubah seiring waktu.

Beberapa orang telah menunjukkan bahwa kedua strategi ini bergantung pada data tertentu, dan unit test hanya menguji fungsionalitas. Untuk itu, saya telah melihat saran:

  • Gunakan server database tiruan, dan periksa hanya bahwa ORM mengirim pertanyaan yang benar sebagai tanggapan terhadap panggilan metode yang diberikan.

Strategi apa yang telah Anda gunakan untuk menguji aplikasi berbasis database, jika ada? Apa yang terbaik untuk Anda?

friedo
sumber
Saya pikir Anda masih harus memiliki indeks basis data di lingkungan uji untuk kasus-kasus seperti indeks unik.
dtc
Saya biasanya tidak keberatan dengan pertanyaan ini di sini, tetapi jika kita mengikuti aturan, pertanyaan ini bukan untuk stackoverflow melainkan untuk softwareengineering.stackexchange website.
ITExpert

Jawaban:

155

Saya sebenarnya menggunakan pendekatan pertama Anda dengan cukup sukses, tetapi dengan cara yang sedikit berbeda yang saya pikir akan menyelesaikan beberapa masalah Anda:

  1. Simpan seluruh skema dan skrip untuk membuatnya dalam kontrol sumber sehingga siapa pun dapat membuat skema database saat ini setelah check out. Selain itu, simpan sampel data dalam file data yang bisa dimuat oleh bagian dari proses pembuatan. Saat Anda menemukan data yang menyebabkan kesalahan, tambahkan ke data sampel Anda untuk memeriksa bahwa kesalahan tidak muncul kembali.

  2. Gunakan server integrasi berkelanjutan untuk membangun skema basis data, memuat data sampel, dan menjalankan tes. Ini adalah cara kami menjaga sinkronisasi basis data pengujian kami (membangunnya kembali di setiap percobaan). Meskipun ini mengharuskan server CI memiliki akses dan kepemilikan contoh database khusus sendiri, saya mengatakan bahwa memiliki skema db kami dibangun 3 kali sehari secara dramatis membantu menemukan kesalahan yang mungkin tidak akan ditemukan sampai sebelum pengiriman (jika tidak nanti ). Saya tidak bisa mengatakan bahwa saya membangun kembali skema sebelum setiap komitmen. Apakah ada Dengan pendekatan ini Anda tidak perlu (well mungkin kita harus, tetapi itu bukan masalah besar jika seseorang lupa).

  3. Untuk grup saya, input pengguna dilakukan pada level aplikasi (bukan db) jadi ini diuji melalui tes unit standar.

Memuat Salinan Basis Data Produksi:
Ini adalah pendekatan yang digunakan pada pekerjaan terakhir saya. Itu adalah penyebab besar dari beberapa masalah:

  1. Salinan akan kedaluwarsa dari versi produksi
  2. Perubahan akan dilakukan pada skema salinan dan tidak akan disebarkan ke sistem produksi. Pada titik ini kita akan memiliki skema yang berbeda. Tidak menyenangkan.

Mengejek Database Server:
Kami juga melakukan ini di pekerjaan saya saat ini. Setelah setiap komit, kami menjalankan tes unit terhadap kode aplikasi yang telah disuntikkan pengakses mock db. Kemudian tiga kali sehari kita menjalankan build db lengkap yang dijelaskan di atas. Saya merekomendasikan kedua pendekatan.

Mark Roddy
sumber
37
Memuat salinan basis data produksi juga memiliki implikasi keamanan dan privasi. Setelah menjadi besar, mengambil salinannya dan memasukkannya ke lingkungan pengembang Anda bisa menjadi masalah besar.
WW.
jujur, ini sangat menyakitkan. Saya baru dalam pengujian dan saya juga menulis sebuah orm yang ingin saya uji. Saya sudah menggunakan metode pertama Anda, tetapi baca bahwa itu tidak membuat unit uji. Saya menggunakan fungsi mesin db tertentu dan mengejek DAO akan sulit. Saya pikir sakit hanya menggunakan metode saya saat ini karena berfungsi dan yang lain menggunakannya. Tes otomatis ya. Terima kasih.
frostymarvelous
2
Saya mengelola dua proyek besar yang berbeda, di salah satu di antaranya pendekatan ini sempurna, tetapi kami telah mengalami banyak masalah dalam mencoba menerapkan ini di proyek lain. Jadi saya pikir itu tergantung pada seberapa mudah dapat dibuat ulang skema setiap kali untuk melakukan tes, saya saat ini sedang berusaha mencari solusi baru untuk masalah yang terakhir ini.
Palang
2
Dalam hal ini, sangat layak untuk menggunakan alat versi database seperti Roundhouse - sesuatu yang dapat menjalankan migrasi. Ini dapat dijalankan pada instance DB apa pun dan harus memastikan bahwa skema terbaru. Selain itu, ketika skrip migrasi ditulis, data pengujian harus ditulis juga menjaga migrasi dan data dalam sinkronisasi.
jedd.ahyoung
lebih baik gunakan monyet menambal dan mengejek dan menghindari operasi penulisan
Nickpick
56

Saya selalu menjalankan tes terhadap DB dalam memori (HSQLDB atau Derby) karena alasan berikut:

  • Itu membuat Anda berpikir data mana yang harus disimpan dalam tes DB Anda dan mengapa. Mengangkut DB produksi Anda ke dalam sistem uji diterjemahkan menjadi "Saya tidak tahu apa yang saya lakukan atau mengapa dan jika sesuatu rusak, itu bukan saya !!" ;)
  • Itu memastikan bahwa database dapat dibuat kembali dengan sedikit usaha di tempat baru (misalnya ketika kita perlu mereplikasi bug dari produksi)
  • Ini sangat membantu dengan kualitas file DDL.

DB dalam memori dimuat dengan data baru setelah tes dimulai dan setelah sebagian besar tes, saya memanggil ROLLBACK agar tetap stabil. SELALU menjaga data dalam uji DB stabil! Jika data berubah sepanjang waktu, Anda tidak dapat menguji.

Data dimuat dari SQL, DB template atau dump / cadangan. Saya lebih suka kesedihan jika mereka berada dalam format yang dapat dibaca karena saya dapat menempatkannya dalam VCS. Jika itu tidak berhasil, saya menggunakan file CSV atau XML. Jika saya harus memuat data dalam jumlah besar ... Saya tidak. Anda tidak perlu memuat data dalam jumlah besar :) Tidak untuk pengujian unit. Tes kinerja adalah masalah lain dan aturan yang berbeda berlaku.

Aaron Digulla
sumber
1
Apakah kecepatan satu-satunya alasan untuk menggunakan (khususnya) DB dalam memori?
rinogo
2
Saya kira keuntungan lain mungkin sifatnya yang "dibuang" - tidak perlu membersihkan diri sendiri; bunuh saja DB dalam memori. (Tapi ada cara lain untuk mencapai ini, seperti pendekatan ROLLBACK yang telah Anda sebutkan)
rinogo
1
Keuntungannya adalah bahwa setiap tes dapat memilih strateginya secara individual. Kami memiliki tes yang melakukan pekerjaan di utas anak yang berarti Spring akan selalu melakukan data.
Aaron Digulla
@ Harun: kami juga mengikuti strategi ini. Saya ingin tahu apa strategi Anda untuk menyatakan bahwa model dalam memori memiliki struktur yang sama dengan db yang asli?
Guillaume
1
@ Guillaume: Saya membuat semua database dari file SQL yang sama. H2 sangat bagus untuk ini karena mendukung sebagian besar keanehan SQL dari database utama. Jika itu tidak berhasil, maka saya menggunakan filter yang mengambil SQL asli dan mengubahnya menjadi SQL untuk database di memori.
Aaron Digulla
14

Saya telah menanyakan pertanyaan ini sejak lama, tetapi saya pikir tidak ada peluru perak untuk itu.

Apa yang saya lakukan saat ini adalah mengejek objek DAO dan menyimpan dalam memori representasi dari koleksi objek yang bagus yang mewakili kasus data yang menarik yang dapat hidup pada database.

Masalah utama yang saya lihat dengan pendekatan itu adalah bahwa Anda hanya menutupi kode yang berinteraksi dengan lapisan DAO Anda, tetapi tidak pernah menguji DAO itu sendiri, dan dalam pengalaman saya, saya melihat bahwa banyak kesalahan terjadi pada lapisan itu juga. Saya juga menyimpan beberapa unit test yang berjalan terhadap basis data (demi menggunakan TDD atau pengujian cepat secara lokal), tetapi tes tersebut tidak pernah berjalan di server integrasi berkelanjutan saya, karena kami tidak menyimpan database untuk tujuan itu dan saya berpikir tes yang berjalan di server CI harus mandiri.

Pendekatan lain yang saya temukan sangat menarik, tetapi tidak selalu bernilai karena sedikit memakan waktu, adalah membuat skema yang sama yang Anda gunakan untuk produksi pada database tertanam yang hanya berjalan dalam unit testing.

Meskipun tidak ada pertanyaan pendekatan ini meningkatkan cakupan Anda, ada beberapa kelemahan, karena Anda harus sedekat mungkin dengan ANSI SQL untuk membuatnya berfungsi baik dengan DBMS Anda saat ini dan penggantian yang disematkan.

Apa pun yang menurut Anda lebih relevan untuk kode Anda, ada beberapa proyek di luar sana yang mungkin membuatnya lebih mudah, seperti DbUnit .

kolrie
sumber
13

Bahkan jika ada alat yang memungkinkan Anda untuk mengejek database Anda dalam satu atau lain cara (misalnya jOOQ 's MockConnection, yang dapat dilihat pada jawaban ini - disclaimer, saya bekerja untuk penjual jOOQ), saya akan menyarankan tidak mengejek database yang lebih besar dengan kompleks pertanyaan.

Bahkan jika Anda hanya ingin menguji-ORM integrasi Anda, berhati-hatilah bahwa ORM mengeluarkan serangkaian pertanyaan yang sangat kompleks ke database Anda, yang mungkin berbeda dalam

  • sintaksis
  • kompleksitas
  • memesan (!)

Mengejek semua itu untuk menghasilkan data dummy yang masuk akal cukup sulit, kecuali jika Anda benar-benar membangun basis data kecil di dalam tiruan Anda, yang menginterpretasikan pernyataan SQL yang dikirimkan. Karena itu, gunakan basis data uji integrasi terkenal yang dapat Anda atur ulang dengan mudah dengan data terkenal, yang dengannya Anda dapat menjalankan tes integrasi.

Lukas Eder
sumber
5

Saya menggunakan yang pertama (menjalankan kode terhadap database uji). Satu-satunya masalah substantif yang saya lihat Anda angkat dengan pendekatan ini adalah kemungkinan skema keluar dari sinkronisasi, yang saya atasi dengan menyimpan nomor versi di basis data saya dan membuat semua perubahan skema melalui skrip yang menerapkan perubahan untuk setiap kenaikan versi.

Saya juga membuat semua perubahan (termasuk ke skema basis data) terhadap lingkungan pengujian saya terlebih dahulu, sehingga akhirnya menjadi sebaliknya: Setelah semua tes lulus, terapkan pembaruan skema ke host produksi. Saya juga menyimpan pasangan pengujian dan basis data aplikasi terpisah pada sistem pengembangan saya sehingga saya dapat memverifikasi di sana bahwa pemutakhiran db berfungsi dengan baik sebelum menyentuh kotak produksi yang sebenarnya.

Dave Sherohman
sumber
3

Saya menggunakan pendekatan pertama tetapi sedikit berbeda yang memungkinkan untuk mengatasi masalah yang Anda sebutkan.

Segala sesuatu yang diperlukan untuk menjalankan tes untuk DAO ada dalam kontrol sumber. Ini termasuk skema dan skrip untuk membuat DB (buruh pelabuhan sangat baik untuk ini). Jika DB yang disematkan dapat digunakan - Saya menggunakannya untuk kecepatan.

Perbedaan penting dengan pendekatan yang dijelaskan lainnya adalah bahwa data yang diperlukan untuk pengujian tidak dimuat dari skrip SQL atau file XML. Semuanya (kecuali beberapa data kamus yang konstan secara efektif) dibuat oleh aplikasi menggunakan fungsi utilitas / kelas.

Tujuan utamanya adalah membuat data yang digunakan dengan tes

  1. sangat dekat dengan ujian
  2. eksplisit (menggunakan file SQL untuk data membuatnya sangat bermasalah untuk melihat data apa yang digunakan oleh tes apa)
  3. mengisolasi tes dari perubahan yang tidak terkait.

Ini pada dasarnya berarti bahwa utilitas ini memungkinkan untuk menentukan secara deklaratif hanya hal-hal penting untuk pengujian dalam tes itu sendiri dan menghilangkan hal-hal yang tidak relevan.

Untuk memberikan beberapa gagasan tentang apa artinya dalam praktik, pertimbangkan tes untuk beberapa DAO yang bekerja dengan Comments to Posts ditulis oleh Authors. Untuk menguji operasi CRUD untuk DAO tersebut, beberapa data harus dibuat dalam DB. Tes akan terlihat seperti:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Ini memiliki beberapa keunggulan dibandingkan skrip SQL atau file XML dengan data uji:

  1. Mempertahankan kode jauh lebih mudah (menambahkan kolom wajib misalnya di beberapa entitas yang direferensikan dalam banyak tes, seperti Penulis, tidak perlu mengubah banyak file / catatan tetapi hanya perubahan pada builder dan / atau pabrik)
  2. Data yang diperlukan oleh tes khusus dijelaskan dalam tes itu sendiri dan tidak dalam beberapa file lain. Kedekatan ini sangat penting untuk uji kelayakan.

Kembalikan vs Komit

Saya merasa lebih nyaman bahwa tes melakukan ketika mereka dijalankan. Pertama, beberapa efek (misalnya DEFERRED CONSTRAINTS) tidak dapat diperiksa jika komit tidak pernah terjadi. Kedua, ketika tes gagal, data dapat diperiksa dalam DB karena tidak dikembalikan oleh rollback.

Karena ini memiliki kelemahan sehingga pengujian dapat menghasilkan data yang rusak dan ini akan menyebabkan kegagalan dalam tes lain. Untuk mengatasinya saya mencoba mengisolasi tes. Pada contoh di atas, setiap pengujian dapat membuat yang baru Authordan semua entitas lain dibuat terkait dengannya sehingga tabrakan jarang terjadi. Untuk menangani invarian yang tersisa yang berpotensi rusak tetapi tidak dapat dinyatakan sebagai batasan level DB, saya menggunakan beberapa pemeriksaan terprogram untuk kondisi yang salah yang dapat dijalankan setelah setiap tes tunggal (dan mereka dijalankan dalam CI tetapi biasanya dimatikan secara lokal untuk kinerja alasan).

Konoval Romawi
sumber
Jika Anda menabur database menggunakan entitas dan orm bukannya skrip sql itu juga memiliki keuntungan bahwa kompiler akan memaksa Anda untuk memperbaiki kode seed jika Anda membuat perubahan pada model Anda. Hanya relevan jika Anda menggunakan bahasa yang diketik statis saja.
daramasala
Jadi untuk klarifikasi: apakah Anda menggunakan fungsi utilitas / kelas di seluruh aplikasi Anda, atau hanya untuk tes Anda?
Ella
@ Ella, fungsi utilitas ini biasanya tidak diperlukan di luar kode uji. Pikirkan misalnya tentang PostBuilder.post(). Ini menghasilkan beberapa nilai untuk semua atribut wajib posting. Ini tidak diperlukan dalam kode produksi.
Roman Konoval
2

Untuk proyek berbasis JDBC (langsung atau tidak langsung, misalnya JPA, EJB, ...) Anda tidak dapat mengejek seluruh basis data (dalam hal demikian akan lebih baik menggunakan tes db pada RDBMS nyata), tetapi hanya mockup di tingkat JDBC .

Keuntungan adalah abstraksi yang datang dengan cara itu, karena data JDBC (set hasil, jumlah pembaruan, peringatan, ...) adalah sama, apa pun backendnya: db produk Anda, db tes, atau hanya beberapa data mockup yang disediakan untuk setiap tes kasus.

Dengan koneksi JDBC yang dipermainkan untuk setiap kasus, tidak perlu mengelola tes db (pembersihan, hanya satu tes pada waktu, memuat ulang perlengkapan, ...). Setiap koneksi mockup terisolasi dan tidak perlu dibersihkan. Hanya perlengkapan minimal yang diperlukan yang disediakan di setiap test case untuk mempermainkan pertukaran JDBC, yang membantu untuk menghindari kerumitan dalam mengelola seluruh test db.

Acolyte adalah kerangka kerja saya yang mencakup driver dan utilitas JDBC untuk jenis mockup ini: http://acolyte.eu.org .

cchantep
sumber