Birim testlerinde boş parametreleri olan nesneler oluşturmak tamam mı?


37

Mevcut projem için bazı birim testleri yazmaya başladım. Bununla birlikte gerçekten bir deneyimim yok. Öncelikle tamamen "almak" istiyorum, bu yüzden şu anda ne IoC çerçevemi ne de alaycı bir kütüphane kullanıyorum.

Birim testlerinde nesnelerin kurucularına boş argümanlar sunarken yanlış bir şey olup olmadığını merak ediyordum. Bazı örnek kod vereyim:

public class CarRadio
{...}


public class Motor
{
    public void SetSpeed(float speed){...}
}


public class Car
{
    public Car(CarRadio carRadio, Motor motor){...}
}


public class SpeedLimit
{
    public bool IsViolatedBy(Car car){...}
}

Yine Bir Başka Araba Kodu Örneği (TM), yalnızca soru için önemli olan kısımlara indirgenmiştir. Şimdi böyle bir şey bir test yazdım:

public class SpeedLimitTest
{
    public void TestSpeedLimit()
    {
        Motor motor = new Motor();
        motor.SetSpeed(10f);
        Car car = new Car(null, motor);

        SpeedLimit speedLimit = new SpeedLimit();
        Assert.IsTrue(speedLimit.IsViolatedBy(car));
    }
}

Test iyi çalışıyor. SpeedLimitBir şey yapmak için bir Carile ihtiyacı var Motor. Hiç ilgilenmiyor CarRadio, bu yüzden boş verdim.

Tamamen oluşturulmadan doğru işlevsellik sağlayan bir nesnenin SRP veya kod kokusu ihlali olup olmadığını merak ediyorum . Bu dırdırcı hislerim var ama speedLimit.IsViolatedBy(motor)haklı çıkmıyor - hız limiti motor tarafından değil, araba tarafından ihlal ediliyor. Belki de ünite testleri ve çalışma kodları için farklı bir perspektife ihtiyacım var, çünkü amacım bütünün sadece bir bölümünü test etmektir.

Birim testlerinde null değerine sahip nesneler oluşturmak bir kod kokusu mu?


24
Biraz konu dışı, ama Motormuhtemelen hiç olmamalıdır speed. Akım ve ' throttlea torquedayalı bir hesaplamaya sahip olmalı . Bunu bir güncel hıza dahil etmek ve onu tekrar sinyal haline getirmek için kullanmak, arabanın işi ... Ama sanırım, zaten gerçekçilik için içinde değildin, değil mi? rpmthrottleTransmissionrpmMotor
cmaster

17
Kod kokusu, kurucunuzun her şeyden önce şikayet etmeden boş kalmasıdır. Bunun kabul edilebilir olduğunu düşünüyorsanız (bunun için nedenleriniz olabilir): devam edin, test edin!
Tommy,

4
Boş Nesne desenini göz önünde bulundurun . Genellikle kodu basitleştirirken, NPE güvenliğini de sağlar.
Bakuriu

17
Tebrikler : Bir nullradyo ile hız limitinin doğru hesaplandığını test ettiniz . Şimdi hız sınırını bir radyo ile doğrulamak için bir test oluşturmak isteyebilirsiniz ; sadece davranış farklı olursa ...
Matthieu M.

3
Bu örnekten emin değilim, ama bazen sadece yarım sınıfı denemek istediğiniz gerçeği, bir şeyin parçalanması gerektiğinin bir ipucudur
Owen

Yanıtlar:


70

Yukarıdaki örnekte, a Carolmadan bir var olabilir CarRadio. Bu durumda, CarRadiobazen sadece bir sıfır boşuna geçmenin kabul edilebilir olmadığını söylemek zorunda kalacağımı, zorunlu olduğunu söyleyebilirim. Testlerinizin Carsınıfın uygulanmasının sağlam olduğundan ve hiçbir CarRadioşey olmadığında boş gösterici istisnaları oluşturmadığından emin olmanız gerekir .

Ancak, farklı bir örnek alalım - bir düşünelim SteeringWheel. Bir tane Carolması gerektiğini varsayalım SteeringWheel, ancak hız testi bunu gerçekten umursamıyor. Bu durumda nullu geçemem SteeringWheelçünkü bunun ardından Carsınıfı gitmek için tasarlanmadığı yerlere itiyor . Bu durumda, DefaultSteeringWheel(metafora devam etmek için) düz bir çizgide kilitli olan bir tür oluştururken daha iyi olursunuz .


44
Ve bir şey Carolmadan inşa edilemediğini kontrol etmek için bir birim testi yazmayı unutma SteeringWheel...
cmaster

13
Bir şeyi özlüyor olabilirim, ancak bir tane olmadan bir oluşturmak mümkün ve hatta yaygın olsa bile , birinin girmesini gerektirmeyen ve açık bir şekilde null hakkında endişelenmenize gerek olmayan bir kurucu oluşturmak mantıklı olmaz mı? ? CarCarRadio
bir kulak

16
Kendi kendine sürüş arabalarında direksiyon yoktur. Sadece söylüyorum. ☺
BlackJack

1
@James_Parsons Ben sadece dahili olarak kullanıldığı ve her zaman boş olmayan parametreleri aldığı için boş denetime sahip olmayan bir sınıfla ilgili olduğunu eklemeyi unuttum. Diğer metotlar CarNRE'yi null değerinde boşaltırdı CarRadio, ancak ilgili kod SpeedLimitolmazdı. Şimdi yayınlanan soru durumunda, sen doğru olurdu ve ben de bunu yapardım.
R. Schmitz

2
@mtraceur Herkesin boş bir argümanla kurgulamaya izin verdiği sınıfın null'un geçerli bir seçenek olduğu varsayımındaki yolu, muhtemelen orada boş denetimler yapmam gerektiğini anlamamı sağladı. O zaman alacağım asıl sorun, burada mahvetmek istemediğim ve bana yardımcı olan birçok iyi, ilginç, iyi yazılmış cevaplar ile kaplıdır. Bu cevabı şimdi de kabul ediyorum, çünkü işleri bitmemiş bırakmaktan hoşlanmıyorum ve en çok oy alan (hepsi yardımcı olmasına rağmen).
R. Schmitz

23

Birim testlerinde null değerine sahip nesneler oluşturmak bir kod kokusu mu?

Evet. C2 kod kokusu tanımına dikkat edin : "Bir CodeSmell, bir şeyin yanlış olabileceğine, kesin değil bir ipucudur."

Test iyi çalışıyor. SpeedLimit'in işini yapabilmesi için Motorlu bir Arabaya ihtiyacı var. Bir CarRadio ile hiç ilgilenmiyor, bu yüzden boş verdim.

Eğer yapıcıya iletilen argümana speedLimit.IsViolatedBy(car)bağlı olmadığını göstermeye çalışıyorsanız CarRadio, daha sonra çağrılırsa testi geçemeyen bir testi iki katına sokarak bu kısıtlamayı açık hale getirmek için gelecekteki bakımcılar için daha net bir şekilde anlaşılır.

Daha muhtemel olduğu gibi, sadece CarRadiobu durumda kullanılmadığını bildiğiniz bir eseri yaratma çalışmalarını kendinizden kurtarmaya çalışıyorsanız , şunu fark etmelisiniz:

  • bu bir kapsülleme ihlalidir ( CarRadiokullanılmayan gerçeğin teste sızmasına izin veriyorsunuz )
  • Carbelirtmeden bir oluşturmak istediğiniz en az bir vaka keşfettinizCarRadio

Kodun size benzeyen bir kurucu istediğinizi söylemeye çalıştığından şüpheliyim.

public Car(IMotor motor) {...}

ve sonra o kurucu , hiçbir şeyin olmadığı gerçeğiyle ne yapılacağına karar verebilir.CarRadio

public Car(IMotor motor) {
    this(null, motor);
}

veya

public Car(IMotor motor) {
    this( new ICarRadio() {...} , motor);
}

veya

public Car(IMotor motor) {
    this( new DefaultRadio(), motor);
}

vb.

SpeedLimit'i test ederken boş bir şeye geçiyorsa. Testin "SpeedLimitTest" olarak adlandırıldığını ve tek Assert'in SpeedLimit yöntemini kontrol ettiğini görebilirsiniz.

Bu ikinci ve ilginç bir koku - SpeedLimit'i test etmek için, SpeedLimit'in kontrol ettiği tek şey hız olsa bile, bir telsizin etrafına bir otomobil yapmak zorundasınız .

Bu , tüm bir aracı talep etmek yerine , aracın uyguladığıSpeedLimit.isViolatedBy bir rol arabirimini kabul etmesi gereken bir ipucu olabilir .

interface IHaveSpeed {
    float speed();
}

class Car implements IHaveSpeed {...}

IHaveSpeedgerçekten berbat bir isim; umarım etki alanı dilinizde bir gelişme olur.

Bu arayüz yerinde olduğunda, testiniz SpeedLimitçok daha kolay olabilir

public void TestSpeedLimit()
{
    IHaveSpeed somethingTravelingQuickly = new IHaveSpeed {
        float speed() { return 10f; }
    }

    SpeedLimit speedLimit = new SpeedLimit();
    Assert.IsTrue(speedLimit.IsViolatedBy(somethingTravelingQuickly));
}

Son örneğinizde, IHaveSpeedarayüzü (en azından c #) bir arayüzün kendisini başlatamayacağınız gibi uygulayan sahte bir sınıf oluşturmanız gerekeceğini sanıyorum .
Ryan Searle

1
Bu doğru - etkili heceleme, testlerinizde hangi Blub lezzetini kullandığınıza bağlı olacaktır.
VoiceOfUnreason

18

Birim testlerinde null değerine sahip nesneler oluşturmak bir kod kokusu mu?

Hayır

Bir şey değil. Aksine, eğer NULLbir argüman olarak kullanılabilir bir değerdir (bazı diller bir boş gösterici arasında geçen reddetmez yapıları olabilir), sen gerektiğini test edin.

Başka bir soru, geçerken ne olduğudur NULL. Fakat tam olarak bir birim testinin konusu budur: her olası giriş için tanımlanmış bir sonucun yapıldığından emin olmak . Ve NULL olan bir tarif edilen sonucu olmalı ve bunun için bir test olmalıdır, böylece bir potansiyel girdi.

Senin Araç Yani eğer sözde bir radyo olmadan constructable olmak, sen gerektiğini bunun için bir test yazmak. O değil ise ve sözde bir özel durum için, sen gerektiğini bunun için bir test yazmak.

Her iki şekilde de, evet gerektiğini için bir test yazmak NULL. Dışarıda bıraksanız bir kod kokusu olurdu. Bu, sihirli bir şekilde hatasız olacağını varsaydığınız test edilmemiş bir kod dalına sahip olduğunuz anlamına gelir.


Lütfen, test edilen kodunuzun geliştirilebileceği diğer cevapları kabul ettiğimi unutmayın. Bununla birlikte, geliştirilmiş kodun işaretçi parametreleri varsa, gelişmiş kod NULLiçin bile geçilmesi iyi bir testtir. NULLMotor olarak geçtiğimde neler olacağını göstermek için bir test yapmak isterdim . Bu bir kod kokusu değil, bir kod kokusunun tam tersi. Test ediyor.


Burada sınama testi yazıyor gibisin Car. Hiçbir şey görmüyorsam yanlış bir ifade olarak kabul ediyorum, soru test ile ilgili SpeedLimit.
R. Schmitz

@ R.Schmitz Ne demek istediğinden emin değilsin. Ben geçen ilgili, soru aktardık NULLkurucusuna Car.
nvoigt

1
Test sırasındaSpeedLimit boş bir şeye geçiyorsa . Testin "SpeedLimitTest" olarak adlandırıldığını ve sadece Assertbir yöntemi kontrol ettiğini görebilirsiniz SpeedLimit. Test Car- örneğin "Otomobiliniz radyo olmadan yapılabiliyorsa, bunun için bir test yazmalısınız" - farklı bir testte olur.
R. Schmitz

7

Şahsen ben boşuna enjekte tereddüt ederdim. Aslında, ne zaman bir kurucuya bağımlılıklar enjekte ettiğimde, yaptığım ilk şeylerden biri, eğer bu enjeksiyonlar boşsa, ArgumentNullException değerini yükseltmektir. Bu sayede etrafımda düzinelerce boş denetime gerek kalmadan bunları sınıfımda güvenle kullanabilirim. İşte çok yaygın bir desen. Yapıcıda ayarlanan bağımlılıkların daha sonra null (veya başka bir şey) olarak ayarlanamayacağından emin olmak için salt okunur kullanımına dikkat edin:

class Car
{
   private readonly ICarRadio _carRadio;
   private readonly IMotor _motor;

   public Car(ICarRadio carRadio, IMotor motor)
   {
      if (carRadio == null)
         throw new ArgumentNullException(nameof(carRadio));

      if (motor == null)
         throw new ArgumentNullException(nameof(motor));

      _carRadio = carRadio;
      _motor = motor;
   }
}

Yukarıdakiler ile, belki boş çeklerimin beklendiği gibi çalıştığından emin olmak için boş değer enjekte ettiğim bir ya da iki tane bir test yapabilirim.

Bunu söyledikten sonra, iki şey yaptığınızda, bu bir sorun olmaz:

  1. Program yerine sınıfları arabirimler için.
  2. Boş değer yerine sahte bağımlılıkları enjekte etmek için sahte bir çerçeve kullanın (Cq için Moq gibi).

Öyleyse, örneğin;

interface ICarRadio
{
   bool Tune(float frequency);
}

interface Motor
{
   bool SetSpeed(float speed);
}

...
var car = new Car(new Moq<ICarRadio>().Object, new Moq<IMotor>().Object);

Moq kullanımıyla ilgili daha fazla ayrıntı burada bulunabilir .

Elbette, ilk önce alay etmeden anlamak istiyorsanız, arayüzleri kullandığınız sürece hala yapabilirsiniz. Basitçe aşağıdaki gibi sahte bir CarRadio ve Motor oluşturun ve bunları yerine enjekte edin:

class DummyCarRadio : ICarRadio
{
   ...
}
class DummyMotor : IMotor
{
   ...
}

...
var car = new Car(new DummyCarRadio(), new DummyMotor())

3
Bu
Liath

1
Aptal nesnelerin de sözleşmelerini yerine getirmek zorunda olduklarını söylemeye bile değildim. Eğer IMotorbir vardı getSpeed()yöntemi, DummyMotorbaşarılı sonra değiştirilmiş hıza dönmek gerekir setSpeedde.
ComFreek

1

Sorun değil, eski kodu bir test donanımına sokmak için iyi bilinen bir bağımlılık kırma tekniğidir. (Bkz. Michael Feathers'ın “Eski Kod ile Etkili Çalışması”.)

Kokuyor mu? İyi...

Test kokusu yok. Test işini yapıyor. “Bu nesne boş olabilir ve test edilen kod hala doğru çalışıyor” şeklinde beyan ediyor.

Koku uygulamada. Bir yapıcı argümanı bildirerek, “ düzgün çalışması için bu sınıfın buna ihtiyacı var ” olduğunu beyan ediyorsunuz . Açıkçası, bu doğru değil. Gerçek olmayan herhangi bir şey biraz koklamak.


1

İlk prensiplere geri dönelim.

Birim testi, tek bir sınıfın bazı yönlerini test eder.

Ancak, diğer sınıfları parametre olarak alan yapıcılar veya yöntemler olacaktır. Bunlarla nasıl başa çıkacağınız durumdan duruma değişecektir ancak kurulum hedefe göre teğet oldukları için minimum düzeyde olmalıdır. NULL parametresini geçmenin tamamen geçerli olmadığı senaryolar olabilir - örneğin radyosu olmayan bir araba.

Ben sadece (araba teması devam etmek) ile başlamak yarış çizgisini test etmekte olduğumuzu, burada tahmin ediyorum, ancak olabilecek ayrıca herhangi defansif kodu ve istisna teslim mekanizmaları olarak çalışıp çalışmadığını kontrol etmek için diğer parametrelere NULL geçmek istiyorum gereklidir.

Çok uzak çok iyi. Ancak, NULL geçerli bir değer değilse . Eh, burada alay, taslak, aptal ve sahte bulanık dünyasındasın .

Bunların üstesinden gelmek biraz zor görünüyorsa, aslında potansiyel olarak kullanılmayacak bir değer olduğundan, Araba yapıcısında NULL değerini geçirerek bir kukla kullanıyorsunuzdur.

Çok hızlı bir şekilde netleşmesi gereken, potansiyel olarak birçok NULL işlemden geçirme ile kodunuzu vurmaya başlamış olmanızdır. Sınıf parametrelerini arayüzlerle değiştirirseniz, bunları işlemeyi çok daha kolay hale getiren alaycı ve DI vb. Yollarını açabilirsiniz.


1
"Birim testleri, tek bir sınıfın kodunu test etmek içindir." Kederlen . Bu, ünite testlerinin ne olduğu değildir ve kılavuzun sizi şişirilmiş sınıflara götürmesine ya da ters üretken, engelleyici testlere götürmesini izlemekten ibaret değildir.
Ant P

3
“ünite” yi “sınıf” için bir örtmece olarak ele almak ne yazık ki çok yaygın bir düşünce şeklidir ancak gerçekte bunların tümü (a) tüm testin geçerliliğini tamamen azaltabilecek “çöp içeri, çöp dışarı” testlerini teşvik etmektedir. süitler ve (b) değişime engel olabilecek, verimliliği azaltan sınırları zorlamak. Keşke daha fazla insan acısını dinlese ve bu önyargıya meydan okursa.
Karınca P

3
Böyle bir karışıklık yok. Davranış birimlerini test edin, kod birimlerini değil. Bir test testinin kavramı hakkında hiçbir şey yoktur - yazılım testinin yaygın kargo kültü kovanı haricinde bir test ünitesinin bir sınıfın açıkça OOP konseptine bağlı olduğu anlamına gelir. Ve bu da çok zarar verici bir yanılgıdır.
Karınca P

1
Ne demek istediğinizi tam olarak bilmiyorum - Ne demek istediğinizi tam tersi tartışıyorum. Saplama / alay zor sınırları (kesinlikle yapmak zorundaysanız) ve davranışsal bir birimi test edin . Genel API aracılığıyla. Bu API ne oluşturduğunuza bağlıdır ancak ayak izi minimum düzeyde olmalıdır. Soyutlama, polimorfizm veya yeniden yapılandırma kodu eklemek, testlerin başarısız olmasına neden olmamalıdır - "sınıf başına birim", bununla çelişir ve ilk etapta testlerin değerini tamamen baltalar.
Ant P

1
RobbieDee - Ben tam tersi hakkında konuşuyorum. Ortak bir yanılgıyı barındıran kişi olabileceğinizi düşünün, “birim” olarak düşündüğünüzü tekrar gözden geçirin ve belki de TDD hakkında konuştuğunda Kent Beck'in gerçekte ne konuştuğunu biraz daha derine dalın. Şimdi çoğu kişinin TDD dediği şey bir kargo kültü canavarlığı. youtube.com/watch?v=EZ05e7EMOLM
Ant P

1

Tasarımınıza bir göz atarak Car, bir kümeden ibaret olduğunu öneririm Features. Bir Özellik belki bir CarRadio, veya EntertainmentSystembir şey CarRadio, DvdPlayervb gibi şeylerden oluşabilir

Yani, en azından, Carbir dizi ile bir inşa etmek zorunda kalacaktı Features. EntertainmentSystemve CarRadioarabiriminin uygulayıcıları (Java, C # etc) public [I|i]nterface Featureolacaktır ve bu da Kompozit tasarım modelinin bir örneği olacaktır.

Bu nedenle, her zaman bir Carbirlikte inşa edersiniz Featuresve bir birim testi hiçbir zaman bir tane Carolmadan başlatmaz Features. BasicTestCarFeatureSetEn basit testiniz için gerekli olan asgari bir işlevsellik kümesi olan bir alaycı düşünebilirsiniz .

Sadece bazı düşünceler.

John

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.