Aynı davranışı gösteren birden çok nesne için birim sınamalarını nasıl yapılandırabilirsiniz?


9

Birçok durumda, bazı davranışları olan mevcut bir sınıf olabilir:

class Lion
{
    public void Eat(Herbivore herbivore) { ... }
}

... ve bir birim testim var ...

[TestMethod]
public void Lion_can_eat_herbivore()
{
    var herbivore = buildHerbivoreForEating();
    var test = BuildLionForTest();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

Şimdi, aslanın kimliğine göre davranışı olan bir Tiger sınıfı yaratmam gerekiyor:

class Tiger
{
    public void Eat(Herbivore herbivore) { ... }
}

... ve aynı davranışı istediğim için, aynı testi çalıştırmam gerekiyor, böyle bir şey yapıyorum:

interface IHerbivoreEater
{
    void Eat(Herbivore herbivore);
}

... ve testimi yeniden düzenledim:

[TestMethod]
public void Lion_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}


public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
    var herbivore = buildHerbivoreForEating();
    var test = builder();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

... ve sonra yeni Tigersınıfım için bir test daha ekliyorum :

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

... sonra benim planı ayrı Lionve Tiger(ama bazen kompozisyon tarafından, genellikle miras yoluyla) sınıfları:

class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }

abstract class HerbivoreEater : IHerbivoreEater
{
    public void Eat(Herbivore herbivore) { ... }
}

... ve her şey yolunda. Bununla birlikte, işlevsellik şimdi HerbivoreEatersınıfta olduğundan, şimdi her alt sınıfta bu davranışların her biri için testlere sahip olmakla ilgili bir sorun var gibi görünüyor. Yine de gerçekte tüketilen alt sınıflar ve üst üste binen davranışları paylaştıkları ( Lionsve Tigerstamamen farklı son kullanımları olabileceği) sadece bir uygulama detayı .

Aynı kodu birden çok kez test etmek gereksiz görünüyor, ancak alt sınıfın temel sınıfın işlevselliğini geçersiz kılabildiği ve geçersiz kıldığı durumlar vardır (evet, LSP'yi ihlal edebilir, ancak yüzleşmesine izin verir IHerbivoreEater, sadece uygun bir test arayüzüdür - son kullanıcı için önemli olmayabilir). Bu testlerin bir değeri var sanırım.

Diğer insanlar bu durumda ne yapar? Testinizi sadece temel sınıfa mı taşıyorsunuz, yoksa tüm alt sınıfları beklenen davranış için mi test ediyorsunuz?

DÜZENLE :

@Pdr tarafından verilen cevaba dayanarak şunu düşünmeliyiz: bu IHerbivoreEatersadece bir yöntem imza sözleşmesidir; davranış belirtmez. Örneğin:

[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}

[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}

[TestMethod]
public void Lion_eats_herbivore_head_first()
{
    IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}

Tartışma uğruna, Animaliçeren bir sınıfınız olmamalı Eatmı? Tüm hayvanlar yer ve bu nedenle Tigerve Lionsınıfı hayvandan miras alabilir.
Muffin Man

1
@Nick - bu iyi bir nokta, ama bence bu farklı bir durum. @Pdr'nin belirttiği gibi, Eatdavranışı temel sınıfa koyarsanız, tüm alt sınıflar aynı Eatdavranışı sergilemelidir . Ancak, bir davranışı paylaşmak için gerçekleşen nispeten ilgisiz 2 sınıftan bahsediyorum. Örneğin, benzer bir uçuş davranışı sergilediğini ve hangi Flydavranışta bulunduğunu varsayabiliriz, ancak bunların ortak bir temel sınıftan türetilmesi mantıklı değildir. BrickPerson
Scott Whitlock

Yanıtlar:


6

Bu harika çünkü testlerin tasarım hakkındaki düşüncelerinizi nasıl yönlendirdiğini gösteriyor. Tasarımda problemler hissediyor ve doğru soruları soruyorsunuz.

Buna bakmanın iki yolu vardır.

IHerbivoreEater bir sözleşmedir. Tüm IHerbivoreEaters bir Herbivore kabul bir Eat yöntemi olmalıdır. Şimdi, testleriniz nasıl yendiğini umursamıyor; Aslanınız kıçlarla başlayabilir ve Tiger boğazda başlayabilir. Tüm testin umurunda, Eat dedikten sonra Otobur yendi.

Öte yandan, söylediğinizin bir kısmı, tüm IHerbivoreEaters'ın Herbivore'u tam olarak aynı şekilde yediği (dolayısıyla temel sınıf). Bu durumda, bir IHerbivoreEater sözleşmesi yapmanın bir anlamı yoktur. Hiçbir şey sunmuyor. Sadece HerbivoreEater'den miras alabilirsiniz.

Ya da belki Aslan ve Kaplan ile tamamen ortadan kaldırın.

Ancak, Aslan ve Kaplan yemek alışkanlıkları dışında her anlamda farklıysa, karmaşık bir miras ağacıyla ilgili sorunlara girip girmeyeceğinizi merak etmeye başlamanız gerekir. Her iki sınıfı da Feline'den veya sadece Lion sınıfını KingOfItsDomain'den (belki Shark ile birlikte) türetmek istiyorsanız ne olur? LSP gerçekten devreye giriyor.

Ortak kod daha iyi kapsüllenmiş öneririz.

public class Lion : IHerbivoreEater
{
    private IHerbivoreEatingStrategy _herbivoreEatingStrategy;
    private Lion (IHerbivoreEatingStrategy herbivoreEatingStrategy)
    {
        _herbivoreEatingStrategy = herbivoreEatingStrategy;
    }

    public Lion() : this(new StandardHerbivoreEatingStrategy())
    {
    }

    public void Eat(Herbivore herbivore)
    {
        _herbivoreEatingStrategy.Eat(herbivore);
    }
}

Aynı şey Tiger için de geçerli.

Şimdi, burada güzel bir şey gelişiyor (güzel, çünkü niyet etmedim). Bu özel kurucuyu sınama için kullanılabilir hale getirirseniz, sahte bir IHerbivoreEatingStrategy'ye geçebilir ve iletinin kapsüllenmiş nesneye düzgün bir şekilde iletildiğini sınayabilirsiniz.

Ve karmaşık testiniz, ilk etapta endişelendiğiniz test, sadece StandardHerbivoreEatingStrategy'yi test etmek zorunda. Bir sınıf, bir test seti, endişelenecek yinelenen kod yok.

Ve daha sonra, Kaplanlara otçullarını farklı bir şekilde yemeleri gerektiğini söylemek isterseniz, bu testlerin hiçbirinin değişmesi gerekmiyor. Basitçe yeni bir HerbivoreEatingStrateji yaratıyorsunuz ve test ediyorsunuz. Kablo bağlantıları entegrasyon testi seviyesinde test edilir.


+1 Strateji Deseni, kafamda soruyu okuyan ilk şeydi.
StuperUser

Çok iyi, ama şimdi sorumu "birim testi" yerine "entegrasyon testi" ile değiştirin. Aynı sorunla karşılaşmıyor muyuz? IHerbivoreEaterbir sözleşmedir, ancak sadece test için ihtiyacım olduğu kadar. Öyle görünüyor ki bu ördek yazmanın gerçekten yardımcı olacağı bir durum. Şimdi ikisini de aynı test mantığına göndermek istiyorum. Arayüzün bir davranış sözü vermesi gerektiğini düşünmüyorum. Testler bunu yapmalı.
Scott Whitlock

harika soru, harika cevap. Özel bir kurucuya sahip olmanıza gerek yok; IoC kapsayıcısı kullanarak IHerbivoreEatingStrategy'yi StandardHerbivoreEatingStrategy'ye bağlayabilirsiniz.
azheglov

@ScottWhitlock: "Arayüzün bir davranış vaat etmesi gerektiğini düşünmüyorum. Testler bunu yapmalı." Ben de öyle söylüyorum. Davranış için bir söz verirse, bundan kurtulmalı ve sadece (base-) sınıfını kullanmalısınız. Test için hiç ihtiyacınız yok.
pdr

@azheglov: Kabul ediyorum, ama cevabım yeterince uzun oldu :)
pdr

1

Aynı kodu birden çok kez test etmek gereksiz görünüyor, ancak alt sınıfın temel sınıfın işlevselliğini geçersiz kılabileceği ve geçersiz kıldığı durumlar var

Yuvarlak bir şekilde, bazı testleri atlamak için beyaz kutu bilgisini kullanmanın uygun olup olmadığını soruyorsunuz. Kara kutu perspektifinden Lionve Tigerfarklı sınıflar. Bu yüzden, kodu bilmeyen biri bunları test eder, ancak derin uygulama bilgisine sahip olursanız, sadece bir hayvanı test etmekten kurtulabileceğinizi bilirsiniz.

Birim testleri geliştirmenin nedenlerinden biri, daha sonra yeniden düzenleme yapmanıza izin vermek, ancak aynı kara kutu arayüzünü sürdürmektir . Birim testleri, sınıflarınızın müşterilerle olan sözleşmelerini yerine getirmeye devam etmesini sağlamanıza yardımcı olur veya en azından sizi sözleşmenin nasıl değişebileceğini dikkatli bir şekilde fark etmeye ve düşünmeye zorlar. Bunu kendiniz fark edersiniz Lionveya daha sonraki bir noktada Tigergeçersiz kılabilirsiniz Eat. Bu uzaktan mümkünse, desteklediğiniz her hayvanın aşağıdaki gibi yiyebileceği basit bir birim test testi:

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

yapılması ve yeterli olması çok basit olmalı ve nesnelerin sözleşmelerini yerine getiremediğini tespit edebilmenizi sağlayacaktır.


Bu sorunun gerçekten kara kutuya karşı beyaz kutu testi tercihine bağlı olup olmadığını merak ediyorum. Kara kutu kampına yaslanıyorum, belki de neden böyle olduğumu test ediyorum. Bunu işaret ettiğiniz için teşekkürler.
Scott Whitlock

1

Doğru yapıyorsun. Birim testini, yeni kodunuzun tek bir kullanımının davranış testi olarak düşünün. Bu, üretim kodundan yapacağınız çağrı ile aynıdır.

Bu durumda, tamamen haklısınız; bir Aslan veya Kaplan kullanıcısı, her ikisi de HerbivoreEaters olduklarını ve yöntem için gerçekten çalışan kodun temel sınıfta her ikisi için ortak olduğunu umursamayacak (en azından gerekmeyecek). Benzer şekilde, bir HerbivoreEater özetinin (beton Aslan veya Kaplan tarafından sağlanan) bir kullanıcısı, sahip olduğu umurunda değildir. İlgilendikleri şey, Aslan, Kaplan veya HerbivoreEater'in bilinmeyen somut uygulamasının bir Herbivor'u doğru yiyeceğidir.

Yani, temelde test ettiğiniz şey, bir Aslan'ın istendiği gibi yiyeceği ve bir Kaplan'ın istendiği gibi yiyeceği. Her ikisini de test etmek önemlidir, çünkü her ikisinin de aynı şekilde yediği her zaman doğru olmayabilir; her ikisini de test ederek, değiştirmek istemediğinizin değişmediğinden emin olursunuz. Her ikisi de tanımlanan HerbivoreEaters olduğundan, en azından Çitayı ekleyene kadar, tüm HerbivoreEaters'ın amaçlandığı gibi yiyeceğini de test ettiniz. Testiniz, kodu tamamen kaplar ve yeterince kullanır (bir Herbivore yiyen bir Herbivore yiyerek ne olması gerektiği konusunda beklenen tüm iddiaları da sağlamanız şartıyla).

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.