Test edilebilir kodu destekleyen tasarım ilkeleri nelerdir? (test edilebilir sürüş testi vs testlerle sürüş tasarımı)


54

Çalıştığım projelerin birçoğu, sonraki testlerde ünite testlerini kabus yapan, geliştirme ve ünite testlerini ayrı ayrı ele alıyor. Amacım, yüksek seviye ve düşük seviye tasarım aşamalarında test sırasında akılda tutulmasıdır.

Test edilebilir kodu destekleyen iyi tanımlanmış tasarım ilkeleri olup olmadığını bilmek istiyorum. Son zamanlarda anladığım bir ilke, Bağımlılık enjeksiyonu ve Kontrolün Tersine çevrilmesi yoluyla Bağımlılık Tersine Çevirmedir.

SOLID olarak bilinen bir şey olduğunu okudum. SOLID ilkelerini takip etmenin dolaylı olarak kolayca test edilebilir kodla sonuçlanıp sonuçlanmadığını anlamak istiyorum? Değilse, test edilebilir kodu destekleyen iyi tanımlanmış tasarım ilkeleri var mı?

Test Odaklı Geliştirme olarak bilinen bir şey olduğunun farkındayım. Bununla birlikte, tasarımı testler arasında sürmek yerine, tasarım aşamasında akılda tutularak kod tasarlamaya daha çok ilgi duyuyorum. Umarım bu mantıklı gelir.

Bu konuyla ilgili bir başka soru da, her modül için bir birim test durumu yazabilmek amacıyla var olan bir ürünü / projeyi yeniden hesaba katmanın ve kod ve tasarımda değişiklik yapmanın doğru olup olmadığıdır.



Teşekkür ederim. Sadece yazıyı okumaya başladım ve bu zaten mantıklı geliyor.

1
Bu benim mülakat sorularımdan biri ("Üniteyi kolayca test etmek için nasıl tasarlarsınız?"). Tek tek, bana ünite testi, alaycı / saplama, OOD ve potansiyel olarak TDD'yi anlayıp anlamadıklarını gösterir. Ne yazık ki, cevaplar genellikle "Test veritabanı yap" gibi bir şeydir.
Chris Pitman

Yanıtlar:


56

Evet, SOLID, kolayca test edilebilecek kod tasarlamanın çok iyi bir yoludur. Kısa bir astar olarak:

S - Tek Sorumluluk İlkesi: Bir nesne tam olarak bir şey yapmalı ve kod üssünde o şeyi yapan tek nesne olmalıdır. Örneğin, bir etki alanı sınıfı alın, bir Fatura deyin. Fatura sınıfı, sistemde kullanılan bir faturanın veri yapısını ve iş kurallarını temsil etmelidir. Kod tabanında bir faturayı temsil eden tek sınıf olmalıdır. Bu, bir yöntemin bir amacı olması gerektiğini ve bu ihtiyacı karşılayan kod tabanındaki tek yöntem olması gerektiğini söyleyerek daha da bozulabilir.

Bu prensibi izleyerek, aynı işlevselliği farklı nesneler üzerinde yazmanız gereken test sayısını azaltarak tasarımınızın test edilebilirliğini arttırırsınız ve ayrıca tipik olarak izolasyonda test edilmesi daha kolay olan daha küçük işlevsellik parçalarına sahip olursunuz.

O - Açık / Kapalı Prensip: Bir sınıf uzamaya açık olmalı, ancak değişmeye kapalı olmalıdır . Bir nesne var olduğunda ve doğru çalıştığında, ideal olarak yeni işlevsellik ekleyen değişiklikler yapmak için o nesneye geri dönmeye gerek olmamalıdır. Bunun yerine, nesne, yeni işlevsellik sağlamak için onu türeterek veya içine yeni veya farklı bağımlılık uygulamaları takarak genişletilmelidir. Bu gerilemeyi önler; Nesnenin başka bir yerde kullanıldığı gibi davranışını değiştirmeden yeni işlevselliği ne zaman ve nerede gerekliyse sunabilirsiniz.

Bu ilkeye bağlı kalarak, genellikle kodun "alaycı" tolere etme kabiliyetini arttırırsınız ve ayrıca yeni davranışı öngörmek için testleri yeniden yazmak zorunda kalmazsınız; Bir nesne için mevcut tüm testler hala genişletilmiş uygulama üzerinde çalışmalı, genişletilmiş uygulamayı kullanan yeni işlevsellik için yeni testler de çalışmalıdır.

L - Liskov Değiştirme Prensibi: B sınıfına bağlı olan A sınıfı, farkı bilmeden herhangi bir X: B'yi kullanabilmelidir. Bu, temel olarak, bir bağımlılık olarak kullandığınız her şeyin, bağımlı sınıfın gördüğü gibi benzer davranışa sahip olması gerektiği anlamına gelir. Kısa bir örnek olarak, ConsoleWriter tarafından uygulanan Write (string) ifadesini gösteren bir IWriter arayüzünüz olduğunu varsayalım. Şimdi bunun yerine bir dosyaya yazmak zorundasınız, bu yüzden FileWriter oluşturursunuz. Bunu yaparken, FileWriter'ın ConsoleWriter ile aynı şekilde kullanılabildiğinden emin olmalısınız (yani bağımlıyla etkileşim kurabilmenin tek yolu Yazma (dize) öğesini çağırarak) ve böylece FileWriter'ın yapması gereken ek bilgiler olabilir. iş (yazılacak yol ve dosya gibi) bağımlıdan başka bir yerden sağlanmalıdır.

Bu, test edilebilir kod yazmak için çok önemlidir, çünkü LSP'ye uygun bir tasarım, beklenen davranışı değiştirmeden, herhangi bir noktada gerçek bir şeyle ikame edilmiş "küçük bir kod parçasının güvenle test edilmesine izin veren" alaylı "bir nesneye sahip olabilir sistemin daha sonra fişe takılı gerçek nesnelerle çalışacağını.

I - Arayüz Ayrıştırma Prensibi: Bir arayüz, arayüz tarafından tanımlanan rolün işlevselliğini sağlamak için mümkün olduğu kadar az yönteme sahip olmalıdır . Basitçe söylemek gerekirse, daha küçük arayüzler daha az sayıda büyük arayüzden daha iyidir. Bunun nedeni, büyük bir arabirimin değişmesi için daha fazla nedeni olması ve kod tabanında başka bir yerde gerekmeyebilecek daha fazla değişikliğe neden olmasıdır.

ISS'ye bağlılık, test edilen sistemlerin karmaşıklığını ve bu SUT'ların bağımlılıklarını azaltarak test edilebilirliği artırır. Test ettiğiniz nesne, DoOne (), DoTwo () ve DoThree () arayüzlerini içeren bir IDoThreeThings arayüzüne bağlıysa, nesne yalnızca DoTwo yöntemini kullanıyor olsa bile, üç yöntemi de uygulayan bir nesneyle alay etmelisiniz. Ancak, nesne yalnızca IDoTwo'ya bağlıysa (yalnızca DoTwo'yu gösterir), bu yöntemi olan bir nesneyle daha kolay alay edebilirsiniz.

D - Bağımlılık Tersine Çevirme İlkesi: Betonlar ve soyutlamalar, asla diğer betonlara değil, soyutlamalara dayanmalıdır . Bu prensip doğrudan gevşek bağlanma inancını zorlar. Bir nesne asla bir nesnenin ne olduğunu bilmemelidir; bunun yerine bir nesnenin ne yaptığını umursamalıdır. Bu nedenle, bir nesnenin veya yöntemin özelliklerini ve parametrelerini tanımlarken, arayüzlerin ve / veya soyut temel sınıfların kullanılması, her zaman somut uygulamaların kullanılması için tercih edilmelidir. Bu, kullanımı değiştirmek zorunda kalmadan bir uygulamayı bir başkasıyla değiştirmenize olanak sağlar (ayrıca, DIP ile el ele giden LSP'yi de takip ederseniz).

Yine, bu test edilebilirlik için çok önemlidir, çünkü bir kez daha, nesneyi test ettiği nesneyle test ederken, “üretim” uygulaması yerine bir bağımlılığın sahte bir uygulamasını enjekte etmenize izin verir. üretimde. Bu, "izolasyonda" ünite testi için anahtardır.


16

SOLID olarak bilinen bir şey olduğunu okudum. SOLID ilkelerini takip etmenin dolaylı olarak kolayca test edilebilir kodla sonuçlanıp sonuçlanmadığını anlamak istiyorum?

Doğru uygulanırsa, evet. Jeff'in SOLID ilkelerini çok kısa bir şekilde açıklayan bir blog yazısı var (bahsi geçen podcast de dinlemeye değer), daha uzun açıklamalar sizi atarsa, oraya bir göz atmanızı öneririm.

Tecrübelerime göre, SOLID'in 2 prensibi test edilebilir kod tasarımında büyük rol oynamaktadır:

  • Arabirim ayrıştırma ilkesi - daha az genel amaçlı olan yerine, müşteriye özgü arabirimleri tercih etmelisiniz. Bu, Tek Sorumluluk Prensibi ile eşleşir ve karşılığında daha kolay test edilebilecek (daha genel olanlara veya çoğunlukla " kötüye kullanma " ve "bağlamlara" göre ) daha az bağımlılık olan özellik / görev odaklı sınıflar tasarlamanıza yardımcı olur. , daha az karmaşıklık, daha iyi taneli, açık testler. Kısacası, küçük bileşenler basit testlere neden olmaktadır.
  • Bağımlılık evrimi ilkesi - uygulama ile değil sözleşmeyle tasarım. Bu, karmaşık nesneleri test ederken ve sadece ayarlamak için tüm bağımlılık grafiğine ihtiyaç duymadığınızı fark ettiğinizde size en çok fayda sağlayacak , ancak basitçe arayüzü taklit edip bununla bitebilirsiniz.

Test edilebilirlik için tasarım yaparken bu ikisinin size en çok yardımcı olacağına inanıyorum. Kalanların da bir etkisi var ama o kadar büyük değil diyebilirim.

(...) mevcut bir ürünü / projeyi yeniden hesaba katmanın ve her modül için bir birim test durumu yazabilmek amacıyla kod ve tasarımda değişiklik yapmanın doğru olup olmadığı?

Mevcut birim testleri olmadan, basitçe söylemek gerekirse - sıkıntılar istiyor. Birim testi, kodunuzun işe yaradığının garantisidir . Uygun test kapsamı varsa, derhal kırılma değişimini tanımak.

Şimdi, birim testleri eklemek amacıyla mevcut kodu değiştirmek istiyorsanız , bu henüz testlerin olmadığı, ancak kodun zaten değiştiği bir boşluk sunar . Doğal olarak, değişikliklerinizi neyin kırdığı hakkında hiçbir fikriniz olmayabilir. Bu, kaçınmak istediğiniz durumdur.

Ünite testleri, test edilmesi zor olan kodata rağmen, yine de yazmaya değer. Eğer kodunuz çalışıyorsa , ancak ünite test edilmemişse, uygun çözüm, bunun için testler yazmak ve sonra değişiklikleri tanıtmak olacaktır. Ancak, daha kolay bir şekilde test edilebilir hale getirmek için test edilen kodun değiştirilmesinin, yönetiminizin para harcamak istemeyebileceği bir şey olduğunu unutmayın (büyük olasılıkla işletme değerinin çok azını getirdiğini duyarsınız).


yüksek uyum ve düşük eşleşme
jk.

8

İLK SORU:

KATI gerçekten gitmek yoludur. SOLID kısaltmasının en önemli iki yönünün, test edilebilirlik söz konusu olduğunda S (Tek Sorumluluk) ve D (Bağımlılık Enjeksiyonu) olduğunu buldum.

Tek Sorumluluk : Sınıfların gerçekten sadece bir şeyi, sadece bir şeyi yapmalı. bir dosya oluşturan, bazı girdilerin ayrıştırıldığı ve onu dosyaya yazdığı bir sınıf zaten üç şey yapıyor. Sınıfınız yalnızca bir şeyi yaparsa, ne olacağını tam olarak biliyorsunuzdur ve bunun için test durumlarını tasarlamak oldukça kolay olmalıdır.

Bağımlılık Enjeksiyonu (DI): Test ortamını kontrol etmenizi sağlar. Kodunuzda önceden oluşturulmuş nesneler oluşturmak yerine, bunu sınıf yapıcısı veya yöntem çağrısı yoluyla enjekte edersiniz. En ilgi çekici olmayan şey ise, gerçek sınıfları tamamen kontrol ettiğiniz saplamalar veya alaylarla değiştirirsiniz.

İKİNCİ SORU: İdeal olarak, kodunuzu yeniden düzenlemeden önce kodunuzun işleyişini belgeleyen testler yazarsınız. Bu şekilde, yeniden düzenleme işleminizin orijinal kodla aynı sonuçları verdiğini belgeleyebilirsiniz. Ancak, sorununuz işleyen kodun test edilmesi zor olmasıdır. Bu klasik bir durum! Tavsiyem: Ünite testinden önce yeniden yapılanmayı düşünün. Yapabilirsen; çalışma kodu için testler yazıp ardından kodu yeniden yazın ve ardından testleri yeniden uygulayın. Biliyorum, saatlere mal olacak, ama daha da kesinleşmiş olacaksınız; yeniden yapılanmış kodun eskisi gibi. Bunu söyleyerek birçok kez vazgeçtim. Sınıflar o kadar çirkin ve dağınık olabilir ki, yeniden yazma onları test edilebilir hale getirmenin tek yoludur.


4

Gevşek bir eşleşmeye ulaşmaya odaklanan diğer cevaplara ek olarak, karmaşık mantığı test etme hakkında bir kelime söylemek istiyorum.

Bir zamanlar mantığı karmaşık, çok sayıda şartlı ve alanların rolünü anlamanın zor olduğu bir sınıfı test etmek zorunda kaldım.

Bu kodu bir durum makinesini temsil eden birçok küçük sınıfla değiştirdim . Mantığın izlenmesi çok daha kolaylaştı, çünkü eski sınıfın farklı halleri açık hale geldi. Her devlet sınıfı diğerlerinden bağımsızdı ve bu yüzden kolayca test edilebilirdi.

Durumların açık olması, kodun tüm olası yollarının (durum geçişleri) numaralandırılmasını ve böylece her biri için bir birim testi yazılmasını kolaylaştırdı.

Tabii ki, her karmaşık mantık bir durum makinesi olarak modellenemez.


3

SOLID mükemmel bir başlangıç, benim tecrübeme göre, SOLID'in dört özelliği gerçekten birim testleriyle iyi çalışıyor.

  • Tek Sorumluluk İlkesi - Her sınıf bir şeyi ve sadece bir şeyi yapar. Bir değeri hesaplamak, bir dosyayı açmak, bir dizgeyi ayrıştırmak, her neyse. Dolayısıyla, girdi ve çıktıların yanı sıra karar noktalarının miktarı çok az olmalıdır. Bu da test yazmayı kolaylaştırıyor.
  • Liskov ikame prensibi - kodunuzun istenen özelliklerini (beklenen sonuçları) değiştirmeden taslaklarda ve alaylarda yerini alabilmeniz gerekir.
  • Arabirim ayrıştırma ilkesi - temas noktalarını arabirimlerle ayırmak, taslaklar ve alaylar oluşturmak için Moq gibi alaycı bir çerçeve kullanmayı çok kolaylaştırır. Somut sınıflara güvenmek yerine, sadece arayüzü uygulayan bir şeye güveniyorsunuz.
  • Bağımlılık Enjeksiyon Prensibi - Bu taslakları ve alayları kodunuza bir kurucu, bir özellik veya test etmek istediğiniz yöntemde bir parametre ile enjekte etmenize izin veren şeydir.

Ayrıca farklı kalıplara, özellikle de fabrika şablonuna bakardım. Bir arayüz uygulayan somut bir sınıfınız olduğunu varsayalım. Beton sınıfını somutlaştırmak için bir fabrika kuracaksınız, ancak bunun yerine arayüzü geri göndereceksiniz.

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

Testlerinizde, bu sanal yöntemi geçersiz kılmak ve tasarımınızın bir arayüzünü döndürmek için Moq veya başka bir alaycı çerçeve oluşturabilirsiniz. Ancak uygulama kanunu söz konusu olduğunda fabrika değişmedi. Ayrıca uygulama ayrıntılarınızın çoğunu bu şekilde gizleyebilirsiniz, uygulama kodunuz arayüzün nasıl oluşturulduğunu önemsemez, tek umursadığı bir arayüzü geri almaktır.

Bu konuyu biraz genişletmek istiyorsanız , Ünite Testi Sanatı'nı okumanızı şiddetle tavsiye ederim . Bu ilkelerin nasıl kullanılacağına dair bazı güzel örnekler verir ve oldukça hızlı bir şekilde okunur.


1
Bağımlılık, "enjeksiyon" prensibine değil, "inversiyon" prensibine denir.
Mathias Lykkegaard Lorenzen
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.