Dosya sistemi bağımlılığına sahip birim test kodu


138

Bir ZIP dosyası verilen, gereken bir bileşen yazıyorum:

  1. Dosyayı açın.
  2. Sıkıştırılmış dosyalar arasında belirli bir dll bulun.
  3. Bu dll yansıması ile yükleyin ve üzerine bir yöntem çağırmak.

Bu bileşeni birim test etmek istiyorum.

Doğrudan dosya sistemi ile ilgilenen kod yazmak için cazipim:

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

Ancak insanlar sık ​​sık "Dosya sistemine, veritabanına, ağa vb. Dayanan birim testleri yazma" diyorlar.

Bunu birim testi dostu bir şekilde yazacak olsaydım, sanırım şöyle olurdu:

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

Yaşasın! Şimdi test edilebilir; DoIt yöntemine test çiftlerini (sahte) besleyebilirim. Ama ne pahasına olursa olsun? Şimdi bunu sadece test edilebilir yapmak için 3 yeni arayüz tanımlamak zorunda kaldım. Ve tam olarak neyi test ediyorum? Benim DoIt işlevinin bağımlılıkları ile düzgün etkileşim olduğunu test ediyorum. Zip dosyasının düzgün şekilde açıldığını vb. Test etmez.

Artık işlevselliği test ediyormuşum gibi gelmiyor. Sadece sınıf etkileşimlerini test ediyormuşum gibi geliyor.

Benim sorum şudur : Dosya sistemine bağlı bir şeyi test etmenin doğru yolu nedir?

edit .NET kullanıyorum, ancak kavram Java veya yerel kodu da uygulayabilir.


8
İnsanlar ünite testinde dosya sistemine yazma diyorlar, çünkü eğer dosya sistemine yazmak istiyorsanız, bir ünite testini neyin oluşturduğunu anlamazsınız. Birim testi genellikle tek bir gerçek nesne (test edilen birim) ile etkileşime girer ve diğer tüm bağımlılıklar alay edilir ve iletilir. Test sınıfı daha sonra nesnenin yöntemleri ve SADECE içindeki mantıksal yolları doğrulayan test yöntemlerinden oluşur. test edilen ünite.
Christopher Perry

1
sizin durumunuzda birim test gerektiren tek kısmı myDll.InvokeSomeSpecialMethod();nerede hem başarı hem de başarısız durumlarda doğru çalışıp çalışmadığını kontrol ederdi, bu yüzden birim testi olmazdı, DoItancak DllRunner.Runtüm sürecin çalışacağını iki kez kontrol etmek için bir UNIT testini yanlış kullanmanın kabul edilebilir bir yanlış kullanım ve bir birim testini maskeleyen bir entegrasyon testi olacağı için, normal birim test kurallarının kesinlikle uygulanması gerekmez
MikeT

Yanıtlar:


47

Bunda gerçekten yanlış bir şey yok, sadece bir birim testi mi yoksa entegrasyon testi mi diye soruyorsunuz. Sadece dosya sistemiyle etkileşime girerseniz istenmeyen yan etkilerin olmadığından emin olmanız gerekir. Özellikle, kendinizden sonra temizlediğinizden - oluşturduğunuz geçici dosyaları sildiğinizden - ve kullandığınız geçici dosyayla aynı dosya adına sahip olan varolan bir dosyanın üzerine yanlışlıkla yazmadığınızdan emin olun. Mutlak yolları değil, her zaman göreli yolları kullanın.

Testinizi chdir()çalıştırmadan önce ve chdir()daha sonra geçici bir dizine geçmek de iyi bir fikir olacaktır .


27
+1, ancak chdir()süreç genelinde olduğunu unutmayın; bu nedenle, test çerçeveniz veya gelecekteki bir sürümü destekliyorsa, testlerinizi paralel olarak çalıştırabilirsiniz.

69

Yaşasın! Şimdi test edilebilir; DoIt yöntemine test çiftlerini (sahte) besleyebilirim. Ama ne pahasına olursa olsun? Şimdi bunu sadece test edilebilir yapmak için 3 yeni arayüz tanımlamak zorunda kaldım. Ve tam olarak neyi test ediyorum? Benim DoIt işlevinin bağımlılıkları ile düzgün etkileşim olduğunu test ediyorum. Zip dosyasının düzgün şekilde açıldığını vb. Test etmez.

Çiviyi tam kafasına vurdun. Test etmek istediğiniz şey, gerçek bir dosyanın ele alınıp alınamayacağı değil, yönteminizin mantığıdır. Bir dosyanın doğru şekilde açılmış olup olmadığını test etmenize gerek yoktur (bu birim testinde), yönteminiz bunu kabul etmek için alır. Arabirimler tek başına değerlidir, çünkü dolaylı olarak veya açık bir şekilde somut bir uygulamaya dayanmak yerine programlayabileceğiniz soyutlamalar sağlarlar.


12
Belirtildiği gibi test edilebilir DoItfonksiyonun teste bile ihtiyacı yoktur. Soru soran kişinin haklı olarak işaret ettiği gibi, test edilecek önemli bir şey kalmamıştır. Şimdi uygulanması var IZipper, IFileSystemve IDllRunnerihtiyaçları test, ancak test için dışarı alay edilmiş çok şeyler olduğuna!
Ian Goldby

56

Sorunuz geliştiriciler için yeni testlerin en zor kısımlarından birini ortaya koyuyor:

"Neyi test ediyorum?"

Örneğiniz çok ilginç değil çünkü sadece bazı API çağrılarını birbirine yapıştırıyor, bu yüzden bunun için bir birim testi yazacak olursanız, sadece yöntemlerin çağrıldığını iddia edersiniz. Bunun gibi testler, uygulama ayrıntılarınızı testle sıkıca birleştirir. Bu kötü bir durum çünkü artık yönteminizin uygulama ayrıntılarını her değiştirdiğinizde testi değiştirmeniz gerekiyor çünkü uygulama ayrıntılarını değiştirmek testlerinizi kırıyor!

Kötü testlere sahip olmak aslında hiç test yapmamaktan daha kötüdür.

Örneğinizde:

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

Alaycılardan geçebilmenize rağmen, test edilecek yöntemde mantık yoktur. Bunun için bir birim testi yapmayı denediyseniz şöyle görünebilir:

// 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);
}

Tebrikler, temel olarak DoIt()yönteminizin uygulama ayrıntılarını bir teste kopyaladınız . Mutlu bakım.

Testler yazarken NASIL değil, NE test etmek istersiniz . Daha fazla bilgi için Kara Kutu Testi konusuna bakın .

NE senin yöntemin adıdır (ya da en azından olması gerektiği). NASIL tüm yöntem içinde yaşayan küçük uygulama detaylar. İyi testler dışarı takas için izin NASIL bozmadan NE .

Bu şekilde düşünün, kendinize sorun:

"Bu yöntemin uygulama ayrıntılarını değiştirirsem (kamu sözleşmesini değiştirmeden) testlerimi bozar mı?"

Cevabınız evet ise , NE olanı değil, NASIL test edersiniz .

Kodun dosya sistemi bağımlılıklarıyla test edilmesiyle ilgili özel sorunuza cevap vermek için, bir dosyayla devam eden biraz daha ilginç bir şeyiniz olduğunu ve a'nın Base64 kodlu içeriğini byte[]bir dosyaya kaydetmek istediğinizi varsayalım . Kontrol etmek zorunda kalmadan kod doğru şeyi test etmek için bu akışları kullanabilirsiniz nasıl bunu yapar. Bir örnek şöyle olabilir (Java'da):

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");
}

Test kullanır, ByteArrayOutputStreamancak uygulamada (bağımlılık enjeksiyonu kullanarak) gerçek StreamFactory (belki de FileStreamFactory olarak adlandırılır) FileOutputStreambirinden dönecek outputStream()ve yazacaktır File.

writeBuradaki yöntemle ilgili ilginç olan şey, içeriği Base64 kodlu olarak yazmasıydı, bu yüzden test ettik. Metodunuz için DoIt()bu bir entegrasyon testi ile daha uygun bir şekilde test edilecektir .


1
Buradaki mesajınıza katıldığımdan emin değilim. Bu tür bir yöntemi test etmeye gerek olmadığını mı söylüyorsunuz? Yani temelde TDD'nin kötü olduğunu mu söylüyorsunuz? Sanki TDD yaparsanız, önce bir test yazmadan bu yöntemi yazamazsınız. Yoksa yönteminizin test gerektirmeyeceğine dair bir önsezi mi kullanıyorsunuz? TÜM birim test çerçevelerinin bir "doğrulama" özelliği içermesinin nedeni, onu kullanmanın uygun olmamasıdır. "Bu kötü çünkü artık yönteminizin uygulama ayrıntılarını her değiştirdiğinizde testi değiştirmeniz gerekiyor" ... birim test dünyasına hoş geldiniz.
Ronnie

2
Uygulamanın değil, bir yöntemin SÖZLEŞMESİNİ test etmeniz gerekiyor. Söz konusu sözleşmeyi uygulamanız her değiştiğinde testinizi değiştirmeniz gerekiyorsa, hem uygulama kodu tabanınızı hem de test kodu tabanınızı koruyan korkunç bir süre içindesiniz.
Christopher Perry

@ Ronnie körü körüne birim testi uygulamak yardımcı olmuyor. Çok çeşitli nitelikte projeler vardır ve birim testleri hepsinde etkili değildir. Örnek olarak, kodun% 95'inin yan etkilerle ilgili olduğu bir proje üzerinde çalışıyorum (not, bu yan etki-ağır doğa gereksinim gereği , temel karmaşıklık, tesadüfi değil , çok çeşitli durumsal kaynaklar ve çok az manipülasyon ile sunar, bu yüzden neredeyse hiç mantık yoktur). Burada birim testi etkili değildir , entegrasyon testi uygulanır.
Vicky Chijwani

Yan etkiler sisteminizin kenarlarına itilmeli, katmanlar boyunca iç içe geçmemelidir. Kenarlarda, davranış olan yan etkileri test edersiniz. Diğer her yerde, kolayca test edilebilen ve akıl yürütmesi, yeniden kullanımı ve oluşturması kolay yan etkileri olmayan saf işlevlere sahip olmaya çalışmalısınız.
Christopher Perry

24

Kodumu yalnızca birim sınamayı kolaylaştırmak için var olan tür ve kavramlarla kirletmek konusunda kararsızım. Elbette, tasarımı daha temiz ve daha iyi hale getirirse, ama bence bu genellikle böyle değildir.

Bunu benim almak, birim testlerin% 100 kapsama alanı olmayabilecek kadar yapabildiğidir. Aslında, sadece% 10 olabilir. Mesele şu ki, birim testleriniz hızlı olmalı ve harici bağımlılıkları olmamalıdır. "Bu parametre için null değerini ilettiğinizde bu yöntem bir ArgumentNullException özel durumu atar" gibi durumları sınayabilirler.

Daha sonra harici bağımlılıkları ve test uçtan uca senaryoları olabilir entegrasyon testleri (otomatik ve muhtemelen aynı birim test çerçevesi kullanarak) eklemek istiyorum.

Kod kapsamını ölçerken hem birim hem de entegrasyon testlerini ölçüyorum.


5
Evet, seni duyuyorum. O kadar çok ayrıştığınız yere ulaştığınız tuhaf bir dünya var, geriye kalan tek şey soyut nesneler üzerinde yöntem çağrıları. Havadar tüyler. Bu noktaya geldiğinizde, gerçek bir şeyi gerçekten test ediyormuşsunuz gibi gelmiyor. Sadece sınıflar arasındaki etkileşimleri test ediyorsunuz.
Judah Gabriel Himango

6
Bu cevap yanlış yönlendirilmiş. Birim testi buzlanma gibi değil, daha çok şeker gibidir. Pastanın içine pişirilir. Kodunuzu yazmanın bir parçası ... bir tasarım etkinliği. Bu nedenle, kodunuzu asla "test etmeyi kolaylaştıracak" herhangi bir şeyle "kirletmezsiniz" çünkü test kodunuzu yazmanızı kolaylaştıran şeydir. Bir testin yazılmasının zor olduğu zamanın% 99'u, geliştirici kodu testten önce yazdı ve kötü test edilemeyen kod
Christopher Perry

1
@Christopher: benzetmenizi genişletmek için, kekimin şekeri kullanabilmem için bir vanilya dilimini andırmasını istemiyorum. Tüm savunduğum şey pragmatizm.
Kent Boogaart

1
@Christopher: biyografiniz her şeyi söylüyor: "Ben bir TDD zealotum". Öte yandan, ben pragmatikim. TDD'yi uyduğu yerde değil de nerede yapıyorum - cevabımdaki hiçbir şey TDD'yi yapmadığımı göstermiyor, ama öyle olduğunu düşünüyorsunuz. Ve TDD olsun ya da olmasın, testi kolaylaştırmak için büyük miktarda karmaşıklık getirmeyeceğim.
Kent Boogaart

3
@ChristopherPerry OP'nin orijinal sorununun bir TDD biçiminde nasıl çözüleceğini açıklayabilir misiniz? Her zaman bununla karşılaşıyorum; Tek amacı, bu sorudaki gibi harici bir bağımlılıkla bir eylem gerçekleştirmek olan bir işlev yazmam gerekiyor. Öyleyse ilk test yazma senaryosunda bile bu test ne olurdu?
Dax Fohl

8

Dosya sistemine vurmakla ilgili yanlış bir şey yoktur, sadece bir birim test yerine bir entegrasyon testi olduğunu düşünün. Sabit kodlu yolu göreli bir yolla takas ve birim testleri için fermuarlar içeren bir TestData alt klasörü oluşturmak.

Entegrasyon testlerinizin çalışması çok uzun sürerse, hızlı ünite testleriniz kadar sık ​​çalışmamaları için ayırın.

Katılıyorum, bazen etkileşim tabanlı testlerin çok fazla eşleşmeye neden olabileceğini ve genellikle yeterli değer sağlamadığını düşünüyorum. Gerçekten, burada doğru yöntemleri çağırdığınızı doğrulamak değil, dosyanın sıkıştırmasını açmak için test etmek istiyorsunuz.


Ne sıklıkla koştukları çok az endişe vericidir; bunları otomatik olarak bizim için çalıştıran sürekli bir entegrasyon sunucusu kullanıyoruz. Ne kadar sürdüklerini umursamıyoruz. "Ne kadar süre çalıştırılacağı" endişe etmiyorsa, birim ve entegrasyon testleri arasında ayrım yapmak için herhangi bir neden var mı?
Judah Gabriel Himango

4
Pek sayılmaz. Ancak geliştiriciler tüm birim testlerini yerel olarak hızlı bir şekilde yapmak isterse, bunu yapmanın kolay bir yoluna sahip olmak güzel.
JC.

6

Bunun bir yolu, InputStreams'i almak için unzip yöntemini yazmak olacaktır. Daha sonra birim testi ByteArrayInputStream kullanarak bir bayt dizisinden böyle bir InputStream oluşturabilir. Bu bayt dizisinin içeriği, birim test kodunda bir sabit olabilir.


Tamam, bu da akışın enjeksiyonuna izin verir. Bağımlılık enjeksiyonu / IOC. Akış dosyaları içine unzipping, bu dosyalar arasında bir dll yükleme ve bu dll bir yöntem çağırma hakkında nasıl?
Judah Gabriel Himango

3

Teoride değişebilecek belirli bir ayrıntıya (dosya sistemi) bağlı olduğunuz için bu daha çok bir entegrasyon testi gibi görünüyor.

Ben kendi modülü (sınıf, montaj, kavanoz, ne olursa olsun) işletim sistemi ile ilgilenen kodu soyut. Durumunuzda belirli bir DLL yüklemek istiyorsanız, bu nedenle bir IDllLoader arayüzü ve DllLoader sınıfı yapın. Uygulamanızı arayüzü kullanarak DllLoader DLL almak ve bunu test .. .. unzip kodu afterall doğru değil mi?


2

"Dosya sistemi etkileşimlerinin" çerçevenin kendisinde iyi test edildiğini varsayarsak, akışlarla çalışmak için yönteminizi oluşturun ve bunu test edin. FileStream'i açmak ve yönteme aktarmak FileStream.Open, çerçeve yaratıcıları tarafından iyi test edildiğinden testlerinizin dışında bırakılabilir.


Siz ve nsayer aslında aynı öneriye sahipsiniz: kodumu akışlarla çalışır hale getirin. Akış içeriğini dll dosyalarına açmak, bu dll'yi açmak ve içinde bir işlevi çağırmak hakkında ne dersiniz? Orada ne yapardın?
Judah Gabriel Himango

3
@JudahHimango. Bu parçalar mutlaka test edilemeyebilir. Her şeyi test edemezsiniz. Test edilemeyen bileşenleri kendi fonksiyonel bloklarına soyutlayın ve çalışacaklarını varsayın. Bu bloğun çalışma şekliyle ilgili bir hatayla karşılaştığınızda, bunun için bir test hazırlayın ve voila. Birim testi, her şeyi test etmeniz gerektiği anlamına GELMEZ. % 100 kod kapsamı bazı senaryolarda gerçekçi değildir.
Zoran Pavlovic

1

Sınıf etkileşimini ve işlev çağrısını test etmemelisiniz. bunun yerine entegrasyon testini düşünmelisiniz. Dosya yükleme işlemini değil, gerekli sonucu test edin.


1

Birim testi için, test dosyasını projenize (EAR dosyası veya eşdeğeri) dahil etmenizi ve ardından birim testlerinde göreli bir yol kullanmanızı öneririz, yani "../testdata/testfile".

Projeniz doğru şekilde dışa aktarıldığı / içe aktarıldığı sürece birim testinizin çalışması gerekir.


0

Diğerlerinin söylediği gibi, birincisi bir entegrasyon testi olarak iyidir. İkincisi, sadece işlevin gerçekte ne yapması gerektiğini test eder, ki bu tamamen bir birim testin yapması gerekir.

Gösterildiği gibi, ikinci örnek biraz anlamsız görünüyor, ancak işlevin adımlardan herhangi birinde hatalara nasıl yanıt verdiğini test etme fırsatı veriyor. Örnekte herhangi bir hata kontrolünüz yok, ancak gerçek sistemde olabilir ve bağımlılık enjeksiyonu tüm hatalara verilen tüm cevapları test etmenize izin verir. O zaman maliyet buna değecekti.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.