Birkaç nedenden dolayı özel işlevselliği test etmekten hoşlanmıyorum. Bunlar aşağıdaki gibidir (bunlar TLDR çalışanlarının ana noktalarıdır):
- Genellikle bir sınıfın özel yöntemini test etmek istediğinizde, bu bir tasarım kokusudur.
- Bunları herkese açık arayüz üzerinden test edebilirsiniz (bu, onları nasıl test etmek istediğinizdir, çünkü müşteri bunları nasıl arayacak / kullanacaktır). Özel yöntemleriniz için tüm geçen testlerde yeşil ışığı görerek yanlış bir güvenlik hissi alabilirsiniz. Genel arayüzünüz aracılığıyla özel işlevlerinizdeki son durumları test etmek çok daha iyi / daha güvenlidir.
- Özel yöntemleri test ederek ciddi test yinelemeleri (çok benzeyen / çok benzeyen testler) riskiyle karşı karşıyasınız. Gereksinimler değiştiğinde bunun önemli sonuçları vardır, çünkü gereğinden fazla test kırılacaktır. Ayrıca, test takımınız nedeniyle yeniden düzenleme yapmanın zor olduğu bir konuma da getirebilir ... ki bu da en ironidir, çünkü test takımı güvenli bir şekilde yeniden tasarlamanıza ve yeniden yapılandırmanıza yardımcı olmak için oradadır!
Bunların her birini somut bir örnekle açıklayacağım. Görünüşe göre 2) ve 3) biraz karmaşık bir şekilde bağlı, bu yüzden örnekleri benzer, ancak onları özel yöntemleri test etmemeniz için ayrı nedenler olarak görüyorum.
Özel yöntemlerin test edilmesinin uygun olduğu zamanlar vardır, yukarıda listelenen olumsuzlukların farkında olmak sadece önemlidir. Daha sonra daha ayrıntılı olarak ele alacağım.
TDD'nin neden özel yöntemleri test etmek için geçerli bir bahane olmadığını da inceliyorum.
Kötü bir tasarımdan çıkış yolunuzu yeniden düzenleme
Gördüğüm en yaygın (anti) paternlardan biri Michael Feathers'ın "Buzdağı" sınıfı dediği şeydir ( Michael Feathers'ın kim olduğunu bilmiyorsanız, "Eski Kod ile Etkili Çalışma" kitabını satın alın / okuyun. profesyonel bir yazılım mühendisi / geliştiricisi olup olmadığınızı bilmeye değer bir kişi). Bu sorunun ortaya çıkmasına neden olan başka (anti) desenler de var, ancak bu karşılaştığım en yaygın model. "Buzdağı" sınıflarının tek bir genel yöntemi vardır ve geri kalanı özeldir (bu yüzden özel yöntemleri test etmek caziptir). Buna "Buzdağı" sınıfı denir, çünkü genellikle yalnız bir genel yöntem alay eder, ancak işlevselliğin geri kalanı su altında özel yöntemler şeklinde gizlenir.
Örneğin, GetNextToken()
dizeyi art arda çağırarak ve beklenen sonucu döndürdüğünü görerek test etmek isteyebilirsiniz . Bunun gibi bir işlev bir testi garanti eder: özellikle belirteç kurallarınız karmaşıksa bu davranış önemsiz değildir. Bu kadar karmaşık olmadığını farz edelim ve sadece boşlukla sınırlandırılmış jetonlara ip atmak istiyoruz. Yani bir test yazıyorsunuz, belki de böyle bir şeye benziyor (bazı dil agnostik psuedo kodu, umarım fikir açıktır):
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
re = RuleEvaluator(input_string);
ASSERT re.GetNextToken() IS "1";
ASSERT re.GetNextToken() IS "2";
ASSERT re.GetNextToken() IS "test";
ASSERT re.GetNextToken() IS "bar";
ASSERT re.HasMoreTokens() IS FALSE;
}
Aslında çok hoş görünüyor. Değişiklikler yaparken bu davranışı sürdürdüğümüzden emin olmak istiyoruz. Ama GetNextToken()
bir olduğunu özel fonksiyon! Bu yüzden bu şekilde test edemeyiz, çünkü derlenmeyecek (Python gibi bazı komut dillerinin aksine, aslında kamu / özeli zorlayan bir dil kullandığımızı varsayarak). Peki ya RuleEvaluator
Tek Sorumluluk İlkesini (Tek Sorumluluk İlkesini) izleyecek şekilde sınıfı değiştirmeye ne dersiniz ? Örneğin, bir ayrıştırıcı, belirteç ve değerlendiricinin bir sınıfa sıkışmış gibi görünüyor. Bu sorumlulukları ayırmak daha iyi olmaz mıydı? Üstelik, bir Tokenizer
sınıf oluşturursanız , o zaman herkese açık yöntemler HasMoreTokens()
veGetNextTokens()
. RuleEvaluator
Sınıf olabilirdiTokenizer
üye olarak nesne. Şimdi, yukarıdaki ile aynı testi koruyabiliriz, ancak Tokenizer
sınıfı test etmek yerineRuleEvaluator
.
UML'de şöyle görünebilir:
Bu yeni tasarımın modülerliği artırdığına dikkat edin, bu nedenle bu sınıfları sisteminizin diğer bölümlerinde tekrar kullanabilirsiniz (yapamadan özel yöntemler tanım gereği yeniden kullanılamaz). Bu, artan anlaşılabilirlik / konum ile birlikte RuleEvaluator'ı parçalamanın ana avantajıdır.
Test son derece benzer görünecektir, ancak GetNextToken()
yöntem şimdi Tokenizer
sınıfta herkese açık olduğu için bu kez derlenecektir :
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS FALSE;
}
Herkese açık bir arayüz aracılığıyla özel bileşenleri test etme ve test tekrarından kaçınma
Sorununuzu daha az modüler bileşene ayırabileceğinizi düşünmeseniz bile ( bunu yapmaya çalışırsanız zamanın% 95'ini yapabilirsiniz), özel işlevleri genel bir arabirim aracılığıyla test edebilirsiniz. Çoğu zaman özel üyeler, genel arayüz aracılığıyla test edilecekleri için test edilmeye değmez. Gördüğüm çoğu zaman, çok görünen benzer , ancak iki farklı işlevi / yöntemi test eden testlerdir. Ne oluyor biter gereksinimleri değişiklik (ve her zaman yapmak) ne zaman, artık 1 yerine 2 kırık testleri Ve Gerçekten bütün özel yöntemler test varsa, daha fazla gibi 10 kırık testlerini yerine 1. olabileceğini olduğu kısacası , özel işlevleri test etme (FRIEND_TEST
veya herkese açık hale getirme veya yansıma kullanma), aksi takdirde herkese açık bir arayüzle test edilebilen test yinelemesine neden olabilir ve / veya yansıma , genellikle uzun vadede pişman olursunuz.. Bunu gerçekten istemezsiniz, çünkü hiçbir şey sizi test takımınızdan yavaşlatmaz. Geliştirme süresini ve bakım maliyetlerini azaltması gerekiyor! Herkese açık bir arayüz aracılığıyla test edilen özel yöntemleri test ederseniz, test takımı tam tersini yapabilir ve bakım maliyetlerini aktif olarak artırabilir ve geliştirme süresini artırabilir. Özel bir işlevi herkese açık hale getirdiğinizde veyaFRIEND_TEST
Tokenizer
Sınıfın aşağıdaki olası uygulamasını düşünün :
Diyelim ki SplitUpByDelimiter()
dizideki her öğe bir belirteç olacak şekilde bir diziyi döndürmekten sorumludur. Dahası, GetNextToken()
bunun sadece bu vektör üzerinde bir yineleyici olduğunu söyleyelim . Dolayısıyla, herkese açık testiniz şöyle görünebilir:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS false;
}
Michael Feather'ın el yordamları aracı dediğimiz gibi davranalım . Bu, diğer kişilerin özel bölümlerine dokunmanıza izin veren bir araçtır. Bir örnek FRIEND_TEST
googletest'ten veya dil destekliyorsa yansımadan kaynaklanır.
TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
result_array = tokenizer.SplitUpByDelimiter(" ");
ASSERT result.size() IS 4;
ASSERT result[0] IS "1";
ASSERT result[1] IS "2";
ASSERT result[2] IS "test";
ASSERT result[3] IS "bar";
}
Şimdi gereksinimlerin değiştiğini ve tokenleştirmenin çok daha karmaşık hale geldiğini varsayalım. Basit bir dize sınırlayıcının yeterli olmayacağına siz karar verirsiniz Delimiter
ve işi yapmak için bir sınıfa ihtiyacınız vardır . Doğal olarak, bir testin kırılmasını beklersiniz, ancak özel işlevleri test ettiğinizde bu ağrı artar.
Özel yöntemleri test etmek ne zaman uygun olabilir?
Yazılımda "tek beden herkese uyar" yoktur. Bazen "kuralları çiğnemek" iyidir (ve aslında idealdir). Mümkün olduğunda özel işlevselliği test etmemeyi şiddetle savunuyorum. İyi olduğunu düşündüğümde iki ana durum var:
Eski sistemlerle yoğun bir şekilde çalıştım (bu yüzden bu kadar büyük bir Michael Feathers hayranıyım) ve bazen özel işlevselliği test etmenin bazen en güvenli olduğunu güvenle söyleyebilirim. Özellikle "karakterizasyon testlerini" taban çizgisine almak için faydalı olabilir.
Aceleniz var ve şimdi ve burada mümkün olan en hızlı şeyi yapmak zorundasınız. Uzun vadede, özel yöntemleri test etmek istemezsiniz. Ancak, refactorun tasarım sorunlarını ele almasının genellikle biraz zaman alacağını söyleyeceğim. Ve bazen bir hafta içinde kargoya verilir. Sorun değil: hızlı ve kirli yapın ve işi yapmanın en hızlı ve en güvenilir yolu olduğunu düşünüyorsanız, bir groping aracı kullanarak özel yöntemleri test edin. Ancak, yaptığınız şeyin uzun vadede yetersiz olduğunu anlayın ve lütfen ona geri dönmeyi düşünün (veya unutulursa, ancak daha sonra görürseniz düzeltin).
Muhtemelen iyi olduğu başka durumlar da vardır. Bunun iyi olduğunu düşünüyorsanız ve iyi bir gerekçeniz varsa, o zaman yapın. Kimse seni durdurmuyor. Sadece potansiyel maliyetlerin farkında olun.
TDD Bahanesi
Bir kenara, TDD'yi özel yöntemleri test etmek için bir bahane olarak kullanan insanları gerçekten sevmiyorum. TDD uyguluyorum ve TDD'nin sizi bunu yapmaya zorladığını sanmıyorum. Önce testinizi (genel arayüzünüz için) yazabilir ve ardından bu arayüzü tatmin etmek için kod yazabilirsiniz. Bazen genel bir arayüz için bir test yazıyorum ve bir veya iki küçük özel yöntem de yazarak tatmin edeceğim (ancak özel yöntemleri doğrudan test etmiyorum, ancak çalıştıklarını veya genel testimin başarısız olacağını biliyorum. ). Bu özel yöntemin son durumlarını test etmem gerekirse, herkese açık arayüzümden onları vuracak bir sürü test yazacağım.Uç kasaları nasıl vuracağınızı anlayamıyorsanız, bu her biri kendi genel yöntemleriyle küçük bileşenlere yeniden bakmanız gereken güçlü bir işarettir. Bu, özel işlevlerin çok fazla yaptığınız ve sınıfın kapsamı dışında olduğunun bir işaretidir .
Ayrıca, bazen şu anda çiğnemek için çok büyük bir ısırık olan bir test yazıyorum ve bu yüzden "eh ile çalışmak için daha fazla API'm olduğunda daha sonra bu teste geri döneceğim" (Ben Bunu yorumlayacağım ve aklımın arkasında tutacağım). Bu noktada tanıştığım birçok geliştirici, TDD'yi günah keçisi olarak kullanarak özel işlevleri için testler yazmaya başlayacak. "Ah, başka bir teste ihtiyacım var, ama bu testi yazmak için bu özel yöntemlere ihtiyacım olacak. Bu nedenle, bir test yazmadan herhangi bir üretim kodu yazamadığım için, bir test yazmam gerekiyor özel bir yöntem için. " Ancak gerçekten yapmaları gereken şey, mevcut sınıflarına bir grup özel yöntem eklemek / test etmek yerine daha küçük ve yeniden kullanılabilir bileşenlere yeniden düzenleme yapmaktır.
Not:
Kısa bir süre önce GoogleTest kullanarak özel yöntemleri test etme hakkında benzer bir soruyu yanıtladım . Bu cevabı çoğunlukla burada daha fazla dil bilimi olmayacak şekilde değiştirdim.
PS İşte Michael Feathers'ın buzdağı sınıfları ve el yordamları araçları ile ilgili ders: https://www.youtube.com/watch?v=4cVZvoFGJTU