Yeni bir dil, TDD için kolay olacak şekilde sıfırdan tasarlanmışsa nasıl görünürdü?


9

Bazı en yaygın dillerle (Java, C #, Java, vb.) Bazen, kodunuzu tam olarak TDD yapmak istediğinizde, dil ile orantılı çalıştığınız anlaşılmaktadır.

Örneğin, Java ve C # 'da, sınıflarınızın bağımlılıklarını alay etmek isteyeceksiniz ve çoğu alaycı çerçeve, sınıfları değil, arayüzleri taklit etmenizi önerecektir. Bu genellikle tek bir uygulama ile birçok arayüzünüz olduğu anlamına gelir (bu etki daha da belirgindir, çünkü TDD sizi daha fazla sayıda daha küçük sınıf yazmaya zorlar). Somut sınıflarla doğru şekilde alay etmenizi sağlayan çözümler, derleyiciyi değiştirmek veya sınıf yükleyicilerini geçersiz kılmak gibi şeyleri yapar, ki bu oldukça kötüdür.

Öyleyse bir dil, TDD için harika olacak şekilde sıfırdan tasarlanmış olsaydı nasıl olurdu? Muhtemelen bir şekilde dil düzeyinde bağımlılıkları tanımlamak (arayüzleri bir kurucuya aktarmak yerine) ve bir sınıfın arayüzünü açıkça yapmadan ayırmak mümkün mü?


TDD'ye ihtiyaç duymayan bir dile ne dersiniz? blog.8thlight.com/uncle-bob/2011/10/20/Simple-Hickey.html
İş

2
Hiçbir dilin TDD'ye ihtiyacı yoktur . TDD yararlı bir uygulamadır ve Hickey'in noktalarından biri, test etmenizden dolayı düşünmeyi bırakabileceğiniz anlamına gelmez .
Frank Shearar

Test Odaklı Geliştirme, dahili ve harici API'larınızın doğru olmasını sağlamak ve bunu önceden yapmakla ilgilidir . Dolayısıyla Java o olduğunu arayüzleri ile ilgili tüm - Gerçek sınıflar yan ürünleridir.

Yanıtlar:


6

Yıllar önce benzer bir soruyu ele alan bir prototip attım; İşte bir ekran görüntüsü:

Sıfır Düğmesi Testi

Fikir, iddiaların kodun kendisiyle satır içi olması ve tüm testlerin temel olarak her tuşa basmasıydı. Test geçişini yaptığınızda, yöntemin yeşile döndüğünü görürsünüz.


2
Haha, bu inanılmaz! Aslında kod ile birlikte testler yapma fikrini çok seviyorum. .NET'te birim testleri için paralel ad alanlarına sahip ayrı montajlara sahip olmak oldukça sıkıcı (çok iyi nedenler olsa da). Ayrıca, kodun taşınması, testleri otomatik olarak hareket ettirdiği için yeniden düzenlemeyi kolaylaştırır: P
Geoff

Ama testleri orada bırakmak ister misin? Onları üretim kodu için etkinleştirir misiniz? Belki de C için # ifdef'd olabilir, aksi takdirde kod boyutu / çalışma zamanı isabetlerine bakıyoruz.
Mawg, Monica'yı

Tamamen bir prototip. Gerçek olsaydı, performans ve boyut gibi şeyleri düşünmeliyiz, ancak bunun için endişelenmek için çok erken ve bu noktaya ulaşırsak, neyi dışarıda bırakacağımızı seçmek zor olmazdı veya istenirse, iddiaları derlenmiş kodun dışında bırakmak. İlginiz için teşekkürler.
Carl Manaster

5

Statik olarak yazmak yerine dinamik olarak olurdu . Ördek yazması , arabirimlerin statik olarak yazılan dillerde yaptığı işi yapar. Ayrıca, sınıfları çalışma zamanında değiştirilebilir, böylece bir test çerçevesi mevcut sınıflar üzerinde yöntemleri kolayca saplayabilir veya alay edebilir. Ruby böyle bir dildir; rspec , TDD için ilk test çerçevesidir.

Dinamik yazım testi nasıl yardımcı olur?

Dinamik yazarak, alay etmek istediğiniz ortak çalışan nesneyle aynı arabirime (yöntem imzaları) sahip bir sınıf oluşturarak sahte nesneler oluşturabilirsiniz. Örneğin, mesaj gönderen bir sınıfınız olduğunu varsayalım:

class MessageSender
  def send
    # Do something with a side effect
  end
end

Diyelim ki bir MessageSender örneği kullanan bir MessageSenderUser var:

class MessageSenderUser

  def initialize(message_sender)
    @message_sender = message_sender
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

Burada bağımlılık enjeksiyonunun , ünite testinin bir elyafının kullanımına dikkat edin. Buna geri döneceğiz.

MessageSenderUser#do_stuffAramaların iki kez gönderildiğini test etmek istiyorsunuz . Tıpkı statik olarak yazılmış bir dilde yaptığınız gibi, kaç kez sendçağrıldığını sayan sahte bir MessageSender oluşturabilirsiniz . Ancak statik olarak yazılmış bir dilden farklı olarak, arabirim sınıfına ihtiyacınız yoktur. Sadece devam edin ve oluşturun:

class MockMessageSender

  attr_accessor :send_count

  def initialize
    @send_count = 0
  end

  def send
    @send_count += 1
  end

end

Ve testinizde kullanın:

mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)

Tek başına, dinamik olarak yazılan bir dilin "ördek yazması", statik olarak yazılan bir dile kıyasla teste o kadar fazla katkıda bulunmaz. Peki ya sınıflar kapatılmaz, ancak çalışma zamanında değiştirilebilirse ne olur? Bu bir oyun değiştirici. Nasıl olduğunu görelim.

Bir sınıfı test edilebilir yapmak için bağımlılık enjeksiyonu kullanmak zorunda kalmazsanız ne olur?

MessageSenderUser öğesinin yalnızca ileti göndermek için MessageSender'ı kullanacağını ve MessageSender'ın başka bir sınıfla değiştirilmesine izin vermenize gerek olmadığını varsayalım. Tek bir programda bu genellikle böyledir. İleti bağımlılığı olmadan bir MessageSender oluşturup kullanması için MessageSenderUser'ı yeniden yazalım.

class MessageSenderUser

  def initialize
    @message_sender = MessageSender.new
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

MessageSenderUser artık kullanımı daha basit: Bunu oluşturan hiç kimsenin kullanması için bir MessageSender oluşturması gerekmiyor. Bu basit örnekte büyük bir gelişme gibi görünmüyor, ancak şimdi MessageSenderUser'ın birden fazla yerde oluşturulduğunu veya üç bağımlılığı olduğunu hayal edin. Şimdi sistem, ünite testlerini mutlu etmek için etrafta çok fazla geçiş örneğine sahip, çünkü tasarımı mutlaka geliştiriyor.

Açık sınıflar bağımlılık enjeksiyonu olmadan test etmenizi sağlar

Dinamik yazım ve açık sınıfları olan bir dilde yapılan test çerçevesi TDD'yi oldukça güzel hale getirebilir. MessageSenderUser için rspec testinden bir kod snippet'i:

mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff

Bütün test bu. Eğer MessageSenderUser#do_stuffçağırmak değil MessageSender#sendtam iki kez, bu test başarısız olur. Gerçek MessageSender sınıfı asla çağrılmaz: Teste, birisi bir MessageSender oluşturmaya çalıştığında, bunun yerine sahte MessageSender'ımızı almaları gerektiğini söyledik. Bağımlılık enjeksiyonuna gerek yoktur.

Bu kadar basit bir testte çok şey yapmak güzel. Tasarımınız için gerçekten mantıklı olmadığı sürece bağımlılık enjeksiyonunu kullanmak zorunda kalmamak daha hoştur.

Fakat bunun açık sınıflarla ne ilgisi var? Çağrıyı not edin MessageSender.should_receive. MessageSender'ı yazarken #should_receive tanımlayamadık, kim yaptı? Yanıt, sistem sınıflarında bazı dikkatli değişiklikler yapan test çerçevesinin, #should_receive aracılığıyla her nesnede tanımlandığı gibi görünmesini sağlayabilmesidir. Sistem sınıflarını bu şekilde değiştirmenin biraz dikkat gerektirdiğini düşünüyorsanız, haklısınız. Ancak test kütüphanesinin burada yaptığı şey için mükemmel bir şey ve açık sınıflar bunu mümkün kılıyor.


Mükemmel cevap! Sizler benimle dinamik diller hakkında konuşmaya başlıyorsunuz :) Bence ördek yazmanın anahtarı budur.
Geoff

3

Öyleyse bir dil, TDD için harika olacak şekilde sıfırdan tasarlanmış olsaydı nasıl olurdu?

'TDD ile iyi çalışır' kesinlikle bir dili tanımlamak için yeterli değildir, bu yüzden herhangi bir şeye "benzeyebilir". Lisp, Prolog, C ++, Ruby, Python ... seçiminizi yapın.

Dahası, TDD'yi desteklemenin dilin kendisi tarafından en iyi ele alınan bir şey olduğu açık değildir. Elbette, her işlev veya yöntemin ilişkili bir sınamaya sahip olduğu bir dil oluşturabilir ve bu sınamaları keşfetmek ve yürütmek için destek oluşturabilirsiniz. Ancak, birim test çerçeveleri zaten keşif ve yürütme bölümünü güzel bir şekilde ele alıyor ve her işlev için bir test gereksiniminin nasıl temiz bir şekilde ekleneceğini görmek zor. Testlerin de testlere ihtiyacı var mı? Yoksa iki fonksiyon sınıfı var mı - teste ihtiyaç duyan normal fonksiyonlar ve bunlara ihtiyaç duymayan test fonksiyonları? Çok zarif görünmüyor.

Belki TDD'yi araçlar ve çerçevelerle desteklemek daha iyidir. IDE içine oluşturun. Onu teşvik eden bir geliştirme süreci oluşturun.

Ayrıca, bir dil tasarlıyorsanız, uzun vadeli düşünmek iyidir. TDD'nin sadece bir yöntem olduğunu ve herkesin tercih ettiği çalışma yöntemi olmadığını unutmayın. Hayal etmek zor olabilir, ancak daha da iyi yolların gelmesi mümkündür. Bir dil tasarımcısı olarak, insanların bu olduğunda dilinizi terk etmelerini ister misiniz?

Soruyu gerçekten cevaplamak için söyleyebileceğiniz tek şey, böyle bir dilin teste elverişli olmasıdır. Bunun pek bir faydası olmadığını biliyorum, ama bence sorun soruda.


Kabul ediyorum, iyi ifade etmek çok zor bir soru. Demek istediğim, Java / C # gibi diller için mevcut test araçlarının dilin biraz yoluna girdiği ve bazı ekstra / alternatif dil özelliklerinin tüm deneyimi daha zarif hale getireceği (yani 90 için arayüzlere sahip olmayacağı). Sınıflarımın% 'si, sadece daha üst düzey bir tasarım açısından mantıklı olanlar).
Geoff

0

Dinamik olarak yazılan diller açık arabirimler gerektirmez. Bkz. Ruby veya PHP vb.

Öte yandan, Java ve C # veya C ++ gibi statik olarak yazılan diller türleri zorlar ve sizi bu arabirimleri yazmaya zorlar.

Anlamadığım şey, onlarla olan sorunun nedir. Arayüzler tasarımın kilit unsurlarından biridir ve tasarım desenlerinin her yerinde ve SOLID ilkelerine uymada kullanılırlar. Örneğin, PHP'de arayüzleri sıklıkla kullanıyorum çünkü tasarımı açık yapıyorlar ve tasarımı da uyguluyorlar. Öte yandan Ruby'de bir türü zorlamanın bir yolu yok, bu ördek yazılı bir dildir. Ama yine de, oradaki arayüzü hayal etmelisiniz ve doğru bir şekilde uygulamak için zihninizdeki tasarımı soyutlamalısınız.

Bu nedenle, sorunuz ilginç gelse de, bağımlılık enjeksiyon tekniklerini anlama veya uygulama konusunda sorunlarınız olduğu anlamına gelir.

Sorunuzu doğrudan yanıtlamak için Ruby ve PHP, hem birim test çerçevelerinde oluşturulmuş hem de ayrı olarak teslim edilmiş harika alay altyapısına sahiptir (bkz. PHP için Mockery). Bazı durumlarda, bu çerçeveler, açıkça bir bağımlılık enjekte etmeden statik çağrıları veya nesne başlatmalarını taklit etmek gibi önerilerinizi bile yapmanızı sağlar.


1
Arayüzlerin harika ve kilit bir tasarım öğesi olduğuna katılıyorum. Ancak, kodumda sınıfların% 90'ının bir arabirimi olduğunu ve bu arabirimin, sınıfın ve o sınıfın alaylarının sadece iki uygulaması olduğunu görüyorum. Bu teknik olarak tam olarak arayüzlerin noktası olmasına rağmen, bunun yetersiz olduğunu hissetmeye yardımcı olamıyorum.
Geoff

Java ve C # ile alay etmeye çok aşina değilim, ama bildiğim kadarıyla, alaycı bir nesne gerçek nesneyi taklit ediyor. Sıklıkla nesnenin türünde bir parametre kullanarak ve yerine yöntem / sınıfa bir sahte göndererek bağımlılık enjeksiyon yapmak. SomeName işlevi gibi bir şey (AnotherClass $ object = null) {$ this-> anotherObject = $ object? : yeni AnotherClass; } Bu, bir arabirimden türetilmeden bağımlılığı enjekte etmek için sık kullanılan bir numaradır.
Patkos Csaba

1
Burada dinamik diller, soruma göre Java / C # tipi dillere göre avantaj sağlıyor. Somut bir sınıfın tipik bir taklidi aslında sınıfın bir alt sınıfını oluşturacaktır, bu da beton sınıfı yapıcısının çağrılması anlamına gelir, bu kesinlikle kaçınmak istediğiniz bir şeydir (istisnalar vardır, ancak kendi sorunları vardır). Dinamik bir alay sadece ördek yazımından yararlanır, böylece beton sınıfı ile alay arasında bir ilişki yoktur. Python'da çok kod yazıyordum, ama bu TDD günlerimden önceydi, belki de başka bir göz atmanın zamanı geldi!
Geoff
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.