Kode pengujian unit dengan ketergantungan sistem file

138

Saya menulis komponen yang, mengingat file ZIP, perlu:

  1. Buka zip file.
  2. Temukan dll tertentu di antara file yang tidak di-zip.
  3. Muat dll melalui refleksi dan memanggil metode di atasnya.

Saya ingin menguji unit komponen ini.

Saya tergoda untuk menulis kode yang berhubungan langsung dengan sistem file:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

Tetapi orang-orang sering berkata, "Jangan menulis unit test yang bergantung pada sistem file, database, jaringan, dll."

Jika saya menulis ini dengan cara uji unit-friendly, saya kira itu akan terlihat seperti ini:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Yay! Sekarang bisa diuji; Saya bisa memberi makan di test dobel (mengolok-olok) ke metode DoIt. Tetapi berapa biayanya? Saya sekarang harus mendefinisikan 3 antarmuka baru hanya untuk membuat ini dapat diuji. Dan apa, tepatnya, yang saya uji? Saya sedang menguji bahwa fungsi Doit saya berinteraksi dengan baik dengan dependensinya. Itu tidak menguji bahwa file zip tidak di-zip dengan benar, dll.

Saya tidak merasa sedang menguji fungsionalitas lagi. Rasanya seperti saya hanya menguji interaksi kelas.

Pertanyaan saya adalah ini : apa cara yang tepat untuk menguji unit sesuatu yang tergantung pada sistem file?

sunting Saya menggunakan .NET, tetapi konsepnya dapat menerapkan kode Java atau asli juga.

Yehuda Gabriel Himango
sumber
8
Orang-orang mengatakan jangan menulis ke sistem file dalam tes unit karena jika Anda tergoda untuk menulis ke sistem file Anda tidak mengerti apa yang dimaksud dengan tes unit. Tes unit biasanya berinteraksi dengan objek nyata tunggal (unit yang diuji) dan semua dependensi lainnya diejek dan diteruskan. Kelas uji kemudian terdiri dari metode pengujian yang memvalidasi jalur logis melalui metode objek dan HANYA jalur logis di unit yang diuji.
Christopher Perry
1
dalam situasi Anda, satu-satunya bagian yang memerlukan pengujian unit adalah di myDll.InvokeSomeSpecialMethod();mana Anda akan memeriksa apakah itu berfungsi dengan baik dalam situasi sukses dan gagal sehingga saya tidak akan menguji unit DoIttetapi DllRunner.Runmengatakan bahwa menyalahgunakan tes UNIT untuk memeriksa ulang bahwa seluruh proses bekerja akan menjadi penyalahgunaan yang dapat diterima dan karena itu akan menjadi tes integrasi yang menyamarkan uji unit, aturan uji unit normal tidak perlu diterapkan secara ketat
MikeT

Jawaban:

47

Benar-benar tidak ada yang salah dengan ini, hanya pertanyaan apakah Anda menyebutnya uji unit atau tes integrasi. Anda hanya perlu memastikan bahwa jika Anda berinteraksi dengan sistem file, tidak ada efek samping yang tidak diinginkan. Secara khusus, pastikan bahwa Anda membersihkan diri sendiri - hapus semua file sementara yang Anda buat - dan bahwa Anda tidak sengaja menimpa file yang ada yang kebetulan memiliki nama file yang sama dengan file sementara yang Anda gunakan. Selalu gunakan jalur relatif dan bukan jalur absolut.

Ini juga merupakan ide bagus untuk chdir()menjadi direktori sementara sebelum menjalankan tes Anda, dan chdir()kembali sesudahnya.

Adam Rosenfield
sumber
27
+1, akan tetapi perhatikan bahwa chdir()prosesnya luas sehingga Anda dapat mematahkan kemampuan untuk menjalankan tes Anda secara paralel, jika kerangka uji Anda atau versi yang akan datang mendukungnya.
69

Yay! Sekarang bisa diuji; Saya bisa memberi makan di test dobel (mengolok-olok) ke metode DoIt. Tetapi berapa biayanya? Saya sekarang harus mendefinisikan 3 antarmuka baru hanya untuk membuat ini dapat diuji. Dan apa, tepatnya, yang saya uji? Saya sedang menguji bahwa fungsi Doit saya berinteraksi dengan baik dengan dependensinya. Itu tidak menguji bahwa file zip tidak di-zip dengan benar, dll.

Anda telah memukul paku tepat di kepalanya. Apa yang ingin Anda uji adalah logika metode Anda, belum tentu apakah file yang sebenarnya dapat ditangani. Anda tidak perlu menguji (dalam pengujian unit ini) apakah sebuah file tidak di-zip dengan benar, metode Anda menerima begitu saja. Antarmuka berharga dengan sendirinya karena mereka memberikan abstraksi yang dapat Anda program melawan, daripada secara implisit atau eksplisit bergantung pada satu implementasi konkret.

andreas buykx
sumber
12
Fungsi yang dapat diuji DoItseperti yang dinyatakan bahkan tidak perlu pengujian. Seperti yang ditunjukkan oleh si penanya dengan benar, tidak ada yang penting untuk diuji. Sekarang ini adalah implementasi dari IZipper,, IFileSystemdan IDllRunneritu perlu pengujian, tetapi mereka adalah hal-hal yang telah dipermainkan untuk pengujian!
Ian Goldby
56

Pertanyaan Anda memaparkan salah satu bagian paling sulit dari pengujian untuk pengembang yang baru saja masuk ke dalamnya:

"Apa yang harus aku uji?"

Contoh Anda tidak terlalu menarik karena itu hanya menempelkan beberapa panggilan API bersama-sama jadi jika Anda menulis tes unit untuk itu Anda akan berakhir hanya dengan menyatakan bahwa metode dipanggil. Tes seperti ini dengan ketat memasangkan detail implementasi Anda dengan tes. Ini buruk karena sekarang Anda harus mengubah tes setiap kali Anda mengubah detail implementasi metode Anda karena mengubah detail implementasi akan merusak tes Anda!

Memiliki tes buruk sebenarnya lebih buruk daripada tidak memiliki tes sama sekali.

Dalam contoh Anda:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Meskipun Anda dapat mengolok-olok, tidak ada logika dalam metode untuk menguji. Jika Anda mencoba tes unit untuk ini mungkin terlihat seperti ini:

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Selamat, pada dasarnya Anda menyalin-menempelkan detail implementasi DoIt()metode Anda ke dalam tes. Selamat memelihara.

Ketika Anda menulis tes, Anda ingin menguji APA dan bukan BAGAIMANA . Lihat Pengujian Kotak Hitam untuk lebih lanjut.

The APA adalah nama dari metode Anda (atau setidaknya seharusnya). The CARA semua rincian pelaksanaan kecil yang hidup di dalam metode Anda. Tes yang baik memungkinkan Anda untuk menukar BAGAIMANA tanpa melanggar APA .

Pikirkan seperti ini, tanyakan pada diri sendiri:

"Jika saya mengubah detail implementasi metode ini (tanpa mengubah kontrak publik) akankah hal itu melanggar pengujian saya?"

Jika jawabannya adalah ya, Anda menguji BAGAIMANA dan bukan APA .

Untuk menjawab pertanyaan spesifik Anda tentang pengujian kode dengan dependensi sistem file, katakanlah Anda memiliki sesuatu yang sedikit lebih menarik terjadi pada sebuah file dan Anda ingin menyimpan konten yang disandikan Base64 byte[]ke file. Anda dapat menggunakan stream untuk ini untuk menguji bahwa kode Anda melakukan hal yang benar tanpa harus memeriksa bagaimana melakukannya. Salah satu contoh mungkin seperti ini (di Jawa):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

Tes menggunakan ByteArrayOutputStreamtetapi dalam aplikasi (menggunakan injeksi ketergantungan) StreamFactory nyata (mungkin disebut FileStreamFactory) akan kembali FileOutputStreamdari outputStream()dan akan menulis ke a File.

Apa yang menarik tentang writemetode di sini adalah bahwa ia menulis konten keluar dari Base64 yang dikodekan, jadi itulah yang kami uji. Untuk DoIt()metode Anda , ini akan lebih tepat diuji dengan tes integrasi .

Christopher Perry
sumber
1
Saya tidak yakin saya setuju dengan pesan Anda di sini. Apakah Anda mengatakan bahwa tidak perlu menguji unit metode semacam ini? Jadi Anda pada dasarnya mengatakan TDD itu buruk? Seolah-olah Anda melakukan TDD maka Anda tidak dapat menulis metode ini tanpa menulis tes terlebih dahulu. Atau apakah Anda mengandalkan firasat bahwa metode Anda tidak akan memerlukan tes? Alasan bahwa SEMUA kerangka kerja pengujian unit menyertakan fitur "verifikasi", adalah tidak apa-apa untuk menggunakannya. "Ini buruk karena sekarang Anda harus mengubah tes setiap kali Anda mengubah detail implementasi metode Anda" ... selamat datang di dunia pengujian unit.
Ronnie
2
Anda seharusnya menguji KONTRAK suatu metode, bukan implementasinya. Jika Anda harus mengubah tes Anda setiap kali implementasi kontrak Anda berubah, maka Anda berada dalam waktu yang mengerikan mempertahankan basis kode aplikasi dan basis kode uji.
Christopher Perry
@Ronnie secara membuta menerapkan pengujian unit tidak membantu. Ada proyek-proyek yang sifatnya sangat beragam, dan pengujian unit tidak efektif di semuanya. Sebagai contoh, saya sedang mengerjakan sebuah proyek di mana 95% dari kode adalah tentang efek samping (perhatikan, sifat efek samping-berat ini adalah dengan persyaratan , itu kompleksitas penting, bukan kebetulan , karena mengumpulkan data dari berbagai sumber stateful dan menyajikannya dengan manipulasi yang sangat sedikit, sehingga hampir tidak ada logika murni). Pengujian unit tidak efektif di sini, pengujian integrasi.
Vicky Chijwani
Efek samping harus didorong ke tepi sistem Anda, mereka tidak boleh terjalin di seluruh lapisan. Di tepi Anda menguji efek samping, yang merupakan perilaku. Di tempat lain Anda harus mencoba memiliki fungsi murni tanpa efek samping, yang mudah diuji dan mudah dipikirkan, digunakan kembali, dan dikomposisi.
Christopher Perry
24

Saya enggan mencemari kode saya dengan jenis dan konsep yang hanya ada untuk memfasilitasi pengujian unit. Tentu, jika itu membuat desain lebih bersih dan lebih baik maka bagus, tapi saya pikir itu sering tidak terjadi.

Menurut saya ini adalah bahwa unit test Anda akan melakukan sebanyak mungkin yang mungkin tidak 100% cakupan. Bahkan, mungkin hanya 10%. Intinya adalah, tes unit Anda harus cepat dan tidak memiliki dependensi eksternal. Mereka mungkin menguji kasus seperti "metode ini melempar ArgumentNullException ketika Anda memasukkan nol untuk parameter ini".

Saya kemudian akan menambahkan tes integrasi (juga otomatis dan mungkin menggunakan kerangka pengujian unit yang sama) yang dapat memiliki dependensi eksternal dan menguji skenario end-to-end seperti ini.

Saat mengukur cakupan kode, saya mengukur tes unit dan integrasi.

Kent Boogaart
sumber
5
Ya, aku mendengarmu. Ada dunia aneh yang Anda capai di mana Anda telah dipisahkan begitu banyak, yang tersisa dengan metode pemanggilan objek abstrak. Bulu lapang. Ketika Anda mencapai titik ini, rasanya tidak seperti Anda benar-benar menguji sesuatu yang nyata. Anda hanya menguji interaksi antar kelas.
Yehuda Gabriel Himango
6
Jawaban ini salah arah. Unit testing tidak seperti frosting, ini lebih seperti gula. Itu dipanggang ke dalam kue. Itu bagian dari menulis kode Anda ... kegiatan desain. Karena itu, Anda tidak pernah "mencemari" kode Anda dengan apa pun yang akan "memfasilitasi pengujian" karena pengujian itulah yang memudahkan Anda menulis kode Anda. 99% dari waktu ujian sulit untuk menulis karena pengembang menulis kode sebelum ujian, dan akhirnya menulis kode jahat yang tidak dapat diuji
Christopher Perry
1
@Christopher: untuk memperluas analogi Anda, saya tidak ingin kue saya berakhir menyerupai irisan vanilla supaya saya bisa menggunakan gula. Yang saya advokasi hanyalah pragmatisme.
Kent Boogaart
1
@Christopher: bio Anda mengatakan itu semua: "Saya seorang fanatik TDD". Saya, di sisi lain, saya pragmatis. Saya melakukan TDD di tempat yang cocok dan bukan di tempat yang tidak cocok - tidak ada dalam jawaban saya yang menunjukkan bahwa saya tidak melakukan TDD, meskipun menurut Anda itu sesuai. Dan apakah itu TDD atau tidak, saya tidak akan memperkenalkan kompleksitas dalam jumlah besar demi memfasilitasi pengujian.
Kent Boogaart
3
@ChristopherPerry Bisakah Anda menjelaskan bagaimana menyelesaikan masalah asli OP dengan TDD? Saya mengalami ini sepanjang waktu; Saya perlu menulis fungsi yang tujuan utamanya adalah untuk melakukan suatu tindakan dengan ketergantungan eksternal, seperti dalam pertanyaan ini. Jadi, bahkan dalam skenario write-the-test-first, tes apa itu?
Dax Fohl
8

Tidak ada yang salah dengan memukul sistem file, anggap saja ini sebagai tes integrasi daripada tes unit. Saya akan menukar jalur kode keras dengan jalur relatif dan membuat subfolder TestData untuk berisi ritsleting untuk tes unit.

Jika tes integrasi Anda membutuhkan waktu terlalu lama untuk dijalankan, pisahkan mereka sehingga tes tersebut tidak berjalan sesering tes cepat unit Anda.

Saya setuju, kadang-kadang saya berpikir pengujian berbasis interaksi dapat menyebabkan kopling terlalu banyak dan seringkali berakhir dengan tidak memberikan nilai yang cukup. Anda benar-benar ingin menguji unzipping file di sini bukan hanya memverifikasi Anda memanggil metode yang tepat.

JC.
sumber
Seberapa sering mereka berlari tidak terlalu diperhatikan; kami menggunakan server integrasi berkelanjutan yang secara otomatis menjalankannya untuk kami. Kami tidak terlalu peduli berapa lama waktu yang dibutuhkan. Jika "berapa lama untuk menjalankan" bukan masalah, apakah ada alasan untuk membedakan antara tes unit dan integrasi?
Yehuda Gabriel Himango
4
Tidak juga. Tetapi jika pengembang ingin dengan cepat menjalankan semua unit test secara lokal itu bagus untuk memiliki cara mudah untuk melakukannya.
JC.
6

Salah satu caranya adalah dengan menulis metode unzip untuk mengambil InputStreams. Kemudian unit test dapat membangun InputStream dari array byte menggunakan ByteArrayInputStream. Isi array byte itu bisa berupa konstanta dalam kode uji unit.

tidak doa
sumber
Ok, jadi itu memungkinkan untuk injeksi aliran. Ketergantungan injeksi / IOC. Bagaimana dengan bagian unzipping aliran ke file, memuat dll di antara file-file itu, dan memanggil metode dalam dll itu?
Yehuda Gabriel Himango
3

Ini tampaknya lebih merupakan tes integrasi karena Anda bergantung pada detail spesifik (sistem file) yang dapat berubah, secara teori.

Saya akan abstrak kode yang berhubungan dengan OS ke dalam modul itu sendiri (kelas, perakitan, toples, apa pun). Dalam kasus Anda, Anda ingin memuat DLL tertentu jika ditemukan, jadi buatlah antarmuka IDllLoader dan kelas DllLoader. Sudahkah aplikasi Anda memperoleh DLL dari DllLoader menggunakan antarmuka dan mengujinya .. Anda tidak bertanggung jawab atas kode unzip juga?

keran
sumber
2

Dengan asumsi bahwa "interaksi sistem file" diuji dengan baik dalam kerangka itu sendiri, buat metode Anda untuk bekerja dengan stream, dan uji ini. Membuka FileStream dan meneruskannya ke metode dapat ditinggalkan dari tes Anda, karena FileStream.Open diuji dengan baik oleh pembuat kerangka.

Sunny Milenov
sumber
Anda dan nsayer pada dasarnya memiliki saran yang sama: buat kode saya berfungsi dengan stream. Bagaimana dengan bagian tentang unzipping konten aliran ke file dll, membuka dll itu dan memanggil fungsi di dalamnya? Apa yang akan kamu lakukan di sana?
Yehuda Gabriel Himango
3
@JudahHimango. Bagian-bagian itu mungkin belum tentu dapat diuji. Anda tidak dapat menguji semuanya. Abstraksi komponen yang tidak dapat diuji ke dalam blok fungsional mereka sendiri, dan asumsikan bahwa mereka akan bekerja. Ketika Anda menemukan bug dengan cara blok ini bekerja, kemudian buat tes untuk itu, dan voila. Pengujian unit TIDAK berarti Anda harus menguji semuanya. Cakupan kode 100% tidak realistis dalam beberapa skenario.
Zoran Pavlovic
1

Anda seharusnya tidak menguji interaksi kelas dan pemanggilan fungsi. alih-alih Anda harus mempertimbangkan pengujian integrasi. Uji hasil yang diperlukan dan bukan operasi pemuatan file.

Dror Helper
sumber
1

Untuk pengujian unit, saya sarankan Anda memasukkan file tes dalam proyek Anda (file EAR atau yang setara) kemudian gunakan jalur relatif dalam tes unit yaitu "../testdata/testfile".

Selama proyek Anda diekspor / diimpor dengan benar, maka tes unit Anda akan berfungsi.

James Anderson
sumber
0

Seperti yang orang lain katakan, yang pertama baik-baik saja sebagai tes integrasi. Tes kedua hanya melakukan fungsi yang seharusnya dilakukan, yang harus dilakukan oleh semua unit test.

Seperti yang ditunjukkan, contoh kedua terlihat sedikit tidak berguna, tetapi itu memberi Anda kesempatan untuk menguji bagaimana fungsi merespons kesalahan dalam salah satu langkah. Anda tidak memiliki kesalahan memeriksa dalam contoh, tetapi dalam sistem nyata yang Anda miliki, dan injeksi dependensi akan membiarkan Anda menguji semua respons terhadap kesalahan. Maka biayanya akan sepadan.

David Sykes
sumber