Cara unit menguji objek dengan kueri basis data

153

Saya pernah mendengar bahwa pengujian unit "benar-benar luar biasa", "sangat keren" dan "segala macam hal yang baik" tetapi 70% atau lebih dari file saya melibatkan akses basis data (beberapa membaca dan beberapa menulis) dan saya tidak yakin bagaimana untuk menulis unit test untuk file-file ini.

Saya menggunakan PHP dan Python tapi saya pikir itu adalah pertanyaan yang berlaku untuk sebagian besar / semua bahasa yang menggunakan akses basis data.

Teifion
sumber

Jawaban:

82

Saya sarankan untuk mengejek panggilan Anda ke database. Mock pada dasarnya adalah objek yang terlihat seperti objek yang Anda coba panggil metode, dalam arti bahwa mereka memiliki sifat yang sama, metode, dll tersedia untuk pemanggil. Tetapi alih-alih melakukan tindakan apa pun yang diprogram untuk dilakukan ketika metode tertentu dipanggil, metode itu melompati semuanya, dan hanya mengembalikan hasilnya. Hasil itu biasanya ditentukan oleh Anda sebelumnya.

Untuk mengatur objek Anda untuk diolok-olok, Anda mungkin perlu menggunakan semacam inversi dari pola injeksi kontrol / dependensi, seperti dalam pseudo-code berikut:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Sekarang dalam pengujian unit Anda, Anda membuat tiruan FooDataProvider, yang memungkinkan Anda memanggil metode GetAllFoos tanpa harus benar-benar menekan database.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Skenario mengejek yang umum, singkatnya. Tentu saja Anda mungkin masih ingin menguji unit panggilan database aktual Anda juga, yang Anda harus menekan database.

Doug R
sumber
Saya tahu ini sudah tua tapi bagaimana dengan membuat tabel duplikat ke yang sudah ada di DB. Dengan begitu Anda bisa memastikan panggilan DB berfungsi?
bretterer
1
Saya telah menggunakan PDO PHP sebagai akses tingkat terendah saya ke database, di mana saya mengekstraksi antarmuka. Lalu saya membangun aplikasi layer database sadar di atas itu. Ini adalah lapisan yang menampung semua kueri SQL mentah dan informasi lainnya. Sisa aplikasi berinteraksi dengan basis data tingkat tinggi ini. Saya menemukan ini bekerja cukup baik untuk pengujian unit; Saya menguji halaman aplikasi saya dalam cara mereka berinteraksi dengan database aplikasi. Saya menguji basis data aplikasi saya dalam cara berinteraksi dengan PDO. Saya menganggap PDO berfungsi tanpa bug. Kode sumber: manx.codeplex.com
melegalkan
1
@ bretterer - Membuat tabel duplikat bagus untuk pengujian integrasi. Untuk pengujian unit, Anda biasanya akan menggunakan objek tiruan yang akan memungkinkan Anda untuk menguji unit kode terlepas dari database.
BornToCode
2
Apa gunanya mengejek panggilan basis data dalam pengujian unit Anda? Tampaknya tidak berguna karena Anda dapat mengubah implementasi untuk mengembalikan hasil yang berbeda, tetapi pengujian unit Anda akan (salah).
bmay2
2
@ bmay2 Kamu tidak salah. Jawaban asli saya sudah ditulis sejak lama (9 tahun!) Ketika banyak orang tidak menulis kode mereka dengan cara yang dapat diuji, dan ketika alat pengujian sangat kurang. Saya tidak akan merekomendasikan pendekatan ini lagi. Hari ini saya hanya akan membuat database pengujian dan mengisinya dengan data yang saya butuhkan untuk tes, dan / atau merancang kode saya sehingga saya dapat menguji sebanyak mungkin logika tanpa database sama sekali.
Doug R
25

Idealnya, objek Anda harus bodoh. Misalnya, Anda harus memiliki "lapisan akses data", yang akan Anda ajukan permintaan, yang akan mengembalikan objek. Dengan cara ini, Anda dapat membiarkan bagian itu keluar dari unit test Anda, atau mengujinya secara terpisah.

Jika objek Anda tergabung erat dengan lapisan data Anda, sulit untuk melakukan pengujian unit yang tepat. bagian pertama dari unit test, adalah "unit". Semua unit harus dapat diuji secara terpisah.

Dalam proyek c # saya, saya menggunakan NHibernate dengan lapisan data yang sepenuhnya terpisah. Objek saya hidup dalam model domain inti dan diakses dari lapisan aplikasi saya. Lapisan aplikasi berbicara ke lapisan data dan lapisan model domain.

Lapisan aplikasi juga kadang-kadang disebut "Lapisan Bisnis".

Jika Anda menggunakan PHP, buat satu set kelas khusus untuk akses data HANYA . Pastikan objek Anda tidak tahu bagaimana mereka bertahan dan pasang keduanya di kelas aplikasi Anda.

Pilihan lain adalah menggunakan mocking / stubs.

Sean Chambers
sumber
Saya selalu setuju dengan ini tetapi dalam praktiknya karena tenggat waktu dan "oke, sekarang tambahkan hanya satu fitur lagi, jam 2 siang hari ini" ini adalah salah satu hal yang paling sulit untuk dicapai. Hal semacam ini adalah target utama refactoring, meskipun, jika bos saya pernah memutuskan dia belum memikirkan 50 masalah baru yang muncul yang memerlukan logika dan tabel bisnis yang sama sekali baru.
Darren Ringer
3
Jika objek Anda tergabung erat dengan lapisan data Anda, sulit untuk melakukan pengujian unit yang tepat. bagian pertama dari unit test, adalah "unit". Semua unit harus dapat diuji secara terpisah. penjelasan yang bagus
Amitābha
11

Cara termudah untuk menguji unit objek dengan akses database menggunakan cakupan transaksi.

Sebagai contoh:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Ini akan mengembalikan keadaan database, pada dasarnya seperti rollback transaksi sehingga Anda dapat menjalankan tes sebanyak yang Anda inginkan tanpa efek samping. Kami telah menggunakan pendekatan ini dengan sukses dalam proyek-proyek besar. Proses pembuatan kami membutuhkan waktu agak lama (15 menit), tetapi tidak mengerikan untuk memiliki 1.800 unit tes. Juga, jika waktu pembangunan menjadi masalah, Anda dapat mengubah proses pembuatan untuk memiliki beberapa bangunan, satu untuk membangun src, yang lain akan menyala setelahnya yang menangani pengujian unit, analisis kode, pengemasan, dll ...

BZ.
sumber
1
+1 Menghemat banyak waktu saat unit menguji lapisan Akses Data. Perlu diketahui bahwa TS akan sering membutuhkan MSDTC yang mungkin tidak diinginkan (tergantung pada apakah aplikasi Anda akan membutuhkan MSDTC)
StuartLC
Pertanyaan aslinya adalah tentang PHP, contoh ini tampaknya C #. Lingkungannya sangat berbeda.
melegalkan
2
Penulis pertanyaan menyatakan bahwa ini adalah pertanyaan umum yang berlaku untuk semua bahasa yang ada hubungannya dengan DB.
Vedran
9
dan teman-teman terkasih ini, disebut tes integrasi
AA.
10

Saya mungkin bisa memberi Anda pengalaman kami ketika kami mulai melihat unit menguji proses tingkat menengah kami yang mencakup satu ton operasi "logika bisnis" sql.

Kami pertama kali membuat lapisan abstraksi yang memungkinkan kami untuk "memasukkan" koneksi database yang masuk akal (dalam kasus kami, kami hanya mendukung koneksi tipe ODBC tunggal).

Setelah ini di tempat, kami kemudian dapat melakukan sesuatu seperti ini dalam kode kami (kami bekerja di C ++, tapi saya yakin Anda mendapatkan idenya):

GetDatabase (). ExecuteSQL ("INSERT INTO foo (blah, blah)")

Pada waktu normal, GetDatabase () akan mengembalikan objek yang mengumpankan semua sql kami (termasuk kueri), melalui ODBC langsung ke database.

Kami kemudian mulai mencari di dalam memori database - yang terbaik tampaknya SQLite. ( http://www.sqlite.org/index.html ). Ini sangat sederhana untuk diatur dan digunakan, dan memungkinkan kami subkelas dan menimpa GetDatabase () untuk meneruskan sql ke basis data dalam memori yang dibuat dan dihancurkan untuk setiap pengujian yang dilakukan.

Kami masih dalam tahap awal ini, tapi sejauh ini terlihat bagus, namun kami harus memastikan kami membuat tabel apa saja yang diperlukan dan mengisinya dengan data uji - namun kami telah mengurangi beban kerja di sini dengan membuat satu set fungsi pembantu umum yang dapat melakukan banyak hal untuk kita.

Secara keseluruhan, ini sangat membantu proses TDD kami, karena membuat perubahan yang tampaknya tidak berbahaya untuk memperbaiki bug tertentu dapat memiliki dampak yang cukup aneh pada area lain (sulit dideteksi) pada sistem Anda - karena sifat sql / database.

Jelas, pengalaman kami berpusat di sekitar lingkungan pengembangan C ++, namun saya yakin Anda mungkin bisa mendapatkan pekerjaan serupa di bawah PHP / Python.

Semoga ini membantu.

Alan
sumber
9

Anda harus mengejek akses basis data jika Anda ingin menguji unit Anda di kelas. Lagi pula, Anda tidak ingin menguji basis data dalam unit test. Itu akan menjadi tes integrasi.

Abaikan panggilan dan kemudian masukkan tiruan yang baru saja mengembalikan data yang diharapkan. Jika kelas Anda tidak melakukan lebih dari mengeksekusi kueri, bahkan mungkin tidak layak untuk mengujinya, ...

Martin Klinke
sumber
6

Buku Pola Tes xUnit menjelaskan beberapa cara untuk menangani kode unit-testing yang mengenai database. Saya setuju dengan orang lain yang mengatakan bahwa Anda tidak ingin melakukan ini karena lambat, tetapi kadang-kadang Anda harus melakukannya, IMO. Mengejek koneksi db untuk menguji hal-hal tingkat tinggi adalah ide yang baik, tetapi periksa buku ini untuk saran tentang hal-hal yang dapat Anda lakukan untuk berinteraksi dengan database yang sebenarnya.

Chris Farmer
sumber
4

Opsi yang Anda miliki:

  • Tulis skrip yang akan menghapus basis data sebelum Anda memulai tes unit, kemudian mengisi db dengan set data yang telah ditentukan dan menjalankan tes. Anda juga dapat melakukannya sebelum setiap tes - ini akan lambat, tetapi lebih sedikit kesalahan.
  • Suntikkan basis data. (Contoh dalam pseudo-Java, tetapi berlaku untuk semua bahasa OO)

    Database kelas {
     kueri Hasil publik (kueri string) {... db nyata di sini ...}
    }

    class MockDatabase memperluas Database { kueri Hasil publik (kueri string) { kembalikan "hasil tiruan"; } }

    class ObjectThatUsesDB { ObjectThatUsesDB publik (Database db) { this.database = db; } }

    sekarang dalam produksi Anda menggunakan database normal dan untuk semua tes Anda cukup menyuntikkan database tiruan yang dapat Anda buat ad hoc.

  • Jangan menggunakan DB sama sekali di sebagian besar kode (itu praktik yang buruk pula). Buat objek "database" yang alih-alih kembali dengan hasil akan mengembalikan objek normal (yaitu akan mengembalikan Userbukan tuple {name: "marcin", password: "blah"}) tulis semua tes Anda dengan objek nyata yang dibuat ad hoc dan tulis satu tes besar yang bergantung pada database yang memastikan konversi ini bekerja dengan baik.

Tentu saja pendekatan ini tidak saling eksklusif dan Anda dapat mencampur dan mencocokkannya sesuai kebutuhan.

Marcin
sumber
3

Unit yang menguji akses basis data Anda cukup mudah jika proyek Anda memiliki kohesi yang tinggi dan sambungan yang longgar. Dengan cara ini Anda hanya dapat menguji hal-hal yang dilakukan masing-masing kelas tanpa harus menguji semuanya sekaligus.

Misalnya, jika Anda menguji kelas antarmuka pengguna Anda, tes yang Anda tulis hanya mencoba memverifikasi logika di dalam UI yang berfungsi seperti yang diharapkan, bukan logika bisnis atau tindakan basis data di balik fungsi tersebut.

Jika Anda ingin menguji unit akses database aktual Anda sebenarnya akan berakhir dengan lebih banyak tes integrasi, karena Anda akan bergantung pada tumpukan jaringan dan server database Anda, tetapi Anda dapat memverifikasi bahwa kode SQL Anda melakukan apa yang Anda minta. melakukan.

Kekuatan tersembunyi dari pengujian unit bagi saya secara pribadi adalah bahwa hal itu memaksa saya untuk mendesain aplikasi saya dengan cara yang jauh lebih baik daripada saya tanpa mereka. Ini karena itu benar-benar membantu saya melepaskan diri dari mentalitas "fungsi ini harus melakukan segalanya".

Maaf saya tidak punya contoh kode khusus untuk PHP / Python, tetapi jika Anda ingin melihat .NET contoh saya punya posting yang menjelaskan teknik yang saya gunakan untuk melakukan pengujian yang sama.

Toran Billups
sumber
2

Saya biasanya mencoba untuk memecah tes saya antara menguji objek (dan ORM, jika ada) dan menguji db. Saya menguji objek-sisi hal dengan mengejek panggilan akses data sedangkan saya menguji sisi db hal dengan menguji interaksi objek dengan db yang, dalam pengalaman saya, biasanya cukup terbatas.

Dulu saya merasa frustrasi dengan menulis unit test sampai saya mulai mengejek bagian akses data sehingga saya tidak harus membuat test db atau menghasilkan data uji dengan cepat. Dengan mengejek data, Anda dapat menghasilkan semuanya pada saat dijalankan dan pastikan bahwa objek Anda berfungsi dengan baik dengan input yang dikenal.

akmad
sumber
2

Saya tidak pernah melakukan ini dalam PHP dan saya tidak pernah menggunakan Python, tetapi yang ingin Anda lakukan adalah mengejek panggilan ke database. Untuk melakukan itu, Anda dapat mengimplementasikan beberapa IOC apakah alat pihak ke-3 atau Anda mengelolanya sendiri, maka Anda dapat menerapkan beberapa versi palsu dari pemanggil basis data di mana Anda akan mengontrol hasil panggilan palsu itu.

Bentuk sederhana dari IoC dapat dilakukan hanya dengan mengkodekan ke Antarmuka. Ini memerlukan semacam orientasi objek yang terjadi dalam kode Anda sehingga mungkin tidak berlaku untuk apa yang Anda lakukan (saya katakan bahwa karena yang harus saya lakukan hanyalah menyebutkan PHP dan Python)

Semoga bermanfaat, jika tidak ada yang lain Anda punya istilah untuk mencari sekarang.

codeLes
sumber
2

Saya setuju dengan akses pasca - basis data pertama harus dilucuti ke lapisan DAO yang mengimplementasikan antarmuka. Kemudian, Anda dapat menguji logika Anda terhadap implementasi rintisan dari lapisan DAO.

Chris Marasti-Georg
sumber
2

Anda bisa menggunakan kerangka kerja mengejek untuk mengabstraksi mesin basis data. Saya tidak tahu apakah PHP / Python mendapatkan beberapa tetapi untuk bahasa yang diketik (C #, Java dll) ada banyak pilihan

Ini juga tergantung pada bagaimana Anda mendesain kode akses database tersebut, karena beberapa desain lebih mudah untuk diuji unit daripada yang lain seperti posting sebelumnya telah disebutkan.

chakrit
sumber
2

Menyiapkan data uji untuk unit test bisa menjadi tantangan.

Ketika datang ke Jawa, jika Anda menggunakan API Musim Semi untuk pengujian unit, Anda dapat mengontrol transaksi pada tingkat unit. Dengan kata lain, Anda dapat menjalankan tes unit yang melibatkan pembaruan / sisipan / penghapusan basis data dan kembalikan perubahan. Di akhir eksekusi, Anda meninggalkan semua yang ada di database seperti sebelum memulai eksekusi. Bagi saya, itu sama baiknya dengan yang bisa didapat.

Bino Manjasseril
sumber