Örnek olarak test etme
Yayınlanan: 2022-02-15Testle ilgili her şey hakkında blog serimize devam ediyoruz. Bu blogda, gerçek örneklere odaklanıyoruz.
Bu gönderideki örnekler JUnit 5 ve AssertJ kullanılarak yazılmış olsa da, dersler diğer herhangi bir birim test çerçevesine uygulanabilir.
JUnit, Java için en popüler test çerçevesidir. AssertJ, geliştiricilerin daha anlamlı testler yazmasına yardımcı olan bir Java kitaplığıdır.
Temel test yapısı
Bakacağımız ilk test örneği, 2 sayı eklemek için basit bir hesap makinesidir.
sınıf Hesap MakinesiShould { @Test // 1 geçersiz toplam() { Hesap makinesi hesap makinesi = yeni Hesap Makinesi(); // 2 int sonuç = hesap makinesi.sum(1, 2); // 3 assertThat(sonuç).isEqualTo(3); // 4 } }
Her yöntem adında should
veya test
tekrarından kaçınmak için testler yazarken ClassShould
adlandırma kuralını kullanmayı tercih ederim. Bununla ilgili daha fazla bilgiyi buradan okuyabilirsiniz.
Yukarıdaki test ne işe yarar?
Testi satır satır bölelim:
-
@Test
ek açıklaması, JUnit çerçevesinin hangi yöntemlerin test olarak çalıştırılması gerektiğini bilmesini sağlar. Test sınıfında test olmayanprivate
yöntemlerin olması tamamen normaldir. - Bu, test ortamını hazırladığımız testimizin düzenleme aşamasıdır. Bu test için ihtiyacımız olan tek şey bir
Calculator
örneğine sahip olmak. - Bu, test etmek istediğimiz davranışı tetiklediğimiz harekete geçme aşamasıdır.
- Bu, ne olduğunu ve her şeyin beklendiği gibi çözülüp çözülmediğini incelediğimiz onaylama aşamasıdır.
assertThat(result)
yöntemi AssertJ kitaplığının bir parçasıdır ve birden çok aşırı yüklemeye sahiptir.
Her aşırı yükleme, özel bir Assert
nesnesi döndürür. Döndürülen nesne, assertThat
yöntemine ilettiğimiz nesne için anlamlı olan yöntemlere sahiptir. Bizim durumumuzda bu nesne, Tamsayıları test etme yöntemleriyle birlikte AbstractIntegerAssert
. isEqualTo(3)
, result == 3
olup olmadığını kontrol edecektir. Eğer öyleyse, test geçecek ve aksi takdirde başarısız olacaktır.
Bu blog gönderisinde herhangi bir uygulamaya odaklanmayacağız.
Arrange , Act , Assert is Give , When , Then hakkında düşünmenin başka bir yolu .
sum
uygulamamızı yazdıktan sonra kendimize bazı sorular sorabiliriz:
- Bu testi nasıl geliştirebilirim?
- Ele almam gereken daha fazla test vakası var mı?
- Bir pozitif ve bir negatif sayı eklersem ne olur? İki negatif sayı? Bir olumlu ve bir olumsuz?
- Tamsayı değerini aşarsam ne olur?
Bu durumları ekleyelim ve mevcut test adını biraz geliştirelim.
Uygulamamızda taşmalara izin vermeyeceğiz. sum
taşarsa, bunun yerine bir ArithmeticException
oluşturacağız.
sınıf Hesap MakinesiShould { özel Hesap Makinesi hesap makinesi = yeni Hesap Makinesi(); @Ölçek void sumPositiveNumbers() { int toplam = hesap makinesi.sum(1, 2); assertThat(sum).isEqualTo(3); } @Ölçek void sumNegativeNumbers() { int toplam = hesap makinesi.toplam(-1, -1); assertThat(sum).isEqualTo(-2); } @Ölçek void sumPositiveAndNegativeNumbers() { int toplam = hesap makinesi.sum(1, -2); assertThat(sum).isEqualTo(-1); } @Ölçek geçersiz failWithArithmeticExceptionWhenOverflown() { assertThatThownBy(() -> hesap makinesi.sum(Tamsayı.MAX_VALUE, 1)) .isInstanceOf(ArithmeticException.class); } }
JUnit, her @Test
yöntemini çalıştırmadan önce yeni bir CalculatorShould
örneği oluşturacaktır. Bu, her CalculatorShould
farklı bir calculator
sahip olması gerektiği anlamına gelir, böylece onu her testte örneklememiz gerekmez.
shouldFailWithArithmeticExceptionWhenOverflown
testi, farklı türde bir assert
kullanır. Bir kod parçasının başarısız olup olmadığını kontrol eder. assertThatThrownBy
yöntemi, sağladığımız lambda'yı çalıştıracak ve başarısız olduğundan emin olacaktır. Bildiğimiz gibi, tüm assertThat
yöntemleri, hangi tür istisnanın meydana geldiğini kontrol etmemize izin veren özel bir Assert
döndürür.
Bu, beklediğimizde kodumuzun başarısız olduğunu nasıl test edebileceğimize bir örnektir. Herhangi bir noktada Calculator
yeniden düzenlersek ve bir taşma durumunda ArithmeticException
atmazsa, testimiz başarısız olur.
NesneAna tasarım deseni
Sonraki örnek, bir Person örneğinin geçerli olduğundan emin olmak için bir validator sınıfıdır.
class PersonValidatorShould { private PersonValidator doğrulayıcı = yeni PersonValidator(); @Ölçek void failWhenNameIsNull() { Kişi kişi = yeni Kişi(boş, 20, yeni Adres(...), ...); assertThatThownBy(() -> validator.validate(kişi)) .isInstanceOf(InvalidPersonException.class); } @Ölçek void failWhenAgeIsNegative() { Kişi kişi = yeni Kişi("John", -5, yeni Adres(...), ...); assertThatThownBy(() -> validator.validate(kişi)) .isInstanceOf(InvalidPersonException.class); } }
ObjectMother tasarım deseni, genellikle testten örnekleme ayrıntılarını gizlemek için karmaşık nesneler oluşturan testlerde kullanılır. Birden çok test aynı nesneyi oluşturabilir, ancak üzerinde farklı şeyleri test edebilir.
Test #1, test #2'ye çok benzer. Doğrulamayı özel bir yöntem olarak çıkararak PersonValidatorShould
yeniden düzenleyebilir, ardından yasa dışı Person
örneklerini, hepsinin aynı şekilde başarısız olmasını bekleyerek geçirebiliriz.
class PersonValidatorShould { private PersonValidator doğrulayıcı = yeni PersonValidator(); @Ölçek void failWhenNameIsNull() { mustFailValidation(PersonObjectMother.createPersonWithoutName()); } @Ölçek void failWhenAgeIsNegative() { ShouldFailValidation(PersonObjectMother.createPersonWithNegativeAge()); } private void mustFailValidation(Kişi geçersizKişi) { assertThatThownBy(() -> validator.validate(invalidPerson)) .isInstanceOf(InvalidPersonException.class); } }
Rastgeleliği test etme
Kodumuzdaki rastgeleliği nasıl test etmemiz gerekiyor?
Rastgele Person
örnekleri generateRandom
için createRandom olan bir PersonGenerator
olduğunu varsayalım.
Aşağıdakileri yazarak başlıyoruz:
class PersonGeneratorShould { özel PersonGenerator oluşturucu = yeni PersonGenerator(); @Ölçek geçersiz createValidPerson() { Kişi kişi = generator.generateRandom(); iddia Bu (kişi). } }
Ve sonra kendimize sormalıyız:
- Burada neyi kanıtlamaya çalışıyorum? Bu işlevin ne yapması gerekiyor?
- Oluşturulan kişinin boş olmayan bir örnek olduğunu doğrulamalı mıyım?
- Rastgele olduğunu kanıtlamam gerekir mi?
- Oluşturulan örneğin bazı iş kurallarına uyması gerekiyor mu?
Dependency Injection kullanarak testimizi basitleştirebiliriz.
genel arayüz RandomGenerator { String createRandomString(); int createRandomInteger(); }
PersonGenerator
artık o arayüzün bir örneğini de kabul eden başka bir kurucuya sahip. Varsayılan olarak, java.Random
kullanarak rastgele değerler üreten JavaRandomGenerator
uygulamasını kullanır.
Ancak testte, daha öngörülebilir başka bir uygulama yazabiliriz.
@Ölçek geçersiz createValidPerson() { RandomGenerator randomGenerator = new PredictableGenerator("John Doe", 20); PersonGenerator oluşturucu = yeni PersonGenerator(randomGenerator); Kişi kişi = generator.generateRandom(); assertThat(person).isEqualTo(new Person("John Doe", 20)); }
Bu test, RandomGenerator
herhangi bir ayrıntısına girmeden, RandomGenerator
tarafından belirtildiği gibi, PersonGenerator
rastgele örnekler oluşturduğunu kanıtlar.
JavaRandomGenerator'ı test etmek, JavaRandomGenerator
etrafında basit bir sarmalayıcı olduğundan, gerçekten herhangi bir değer java.Random
. Test ederek, esasen Java standart kitaplığından java.Random
test etmiş olursunuz. Belirgin testler yazmak, yalnızca çok az fayda ile ek bakıma yol açacaktır.
PredictableGenerator
gibi test amaçlı uygulamalar yazmaktan kaçınmak için Mockito gibi bir alay kitaplığı kullanmalısınız.
PredictableGenerator
yazdığımızda, aslında RandomGenerator
sınıfını manuel olarak sapladık. Mockito'yu kullanarak da saplamış olabilirsin:
@Ölçek geçersiz createValidPerson() { RandomGenerator randomGenerator = sahte(RandomGenerator.class); ne zaman(randomGenerator.generateRandomString()).thenReturn("John Doe"); ne zaman(randomGenerator.generateRandomInteger()).thenReturn(20); PersonGenerator oluşturucu = yeni PersonGenerator(randomGenerator); Kişi kişi = generator.generateRandom(); assertThat(person).isEqualTo(new Person("John Doe", 20)); }
Test yazmanın bu yolu daha anlamlıdır ve belirli testler için daha az uygulamaya yol açar.
Mockito, maketler ve taslaklar yazmak için bir Java kütüphanesidir. Kolayca somutlaştıramayacağınız dış kitaplıklara bağlı kodu test ederken çok kullanışlıdır. Doğrudan uygulamadan bu sınıflar için davranış yazmanıza olanak tanır.
Mockito ayrıca, alıştığımıza benzer birden fazla testimiz olduğunda, kalıp levhasını azaltmak için sahte oluşturmak ve enjekte etmek için başka bir sözdizimine izin verir:
@ExtendWith(MockitoExtension.class) // 1 class PersonGeneratorShould { @Sahte // 2 RandomGenerator randomGenerator; @InjectMocks // 3 özel PersonGenerator üreteci; @Ölçek geçersiz createValidPerson() { ne zaman(randomGenerator.generateRandomString()).thenReturn("John Doe"); ne zaman(randomGenerator.generateRandomInteger()).thenReturn(20); Kişi kişi = generator.generateRandom(); assertThat(person).isEqualTo(new Person("John Doe", 20)); } }
1. JUnit 5, yeteneklerini genişletmek için “uzantıları” kullanabilir. Bu ek açıklama, ek açıklamalar aracılığıyla taklitleri tanımasına ve bunları uygun şekilde enjekte etmesine olanak tanır.
2. @Mock
ek açıklaması, alanın alaylı bir örneğini oluşturur. Bu, test yöntemi gövdemizde mock(RandomGenerator.class)
yazmakla aynıdır.
3. @InjectMocks
ek açıklaması, yeni bir PersonGenerator
örneği oluşturacak ve generator
örneğine alaylar enjekte edecektir.
JUnit 5 uzantıları hakkında daha fazla ayrıntı için buraya bakın.
Mockito enjeksiyonu hakkında daha fazla ayrıntı için buraya bakın.
@InjectMocks
kullanmanın bir dezavantajı var. Nesnenin bir örneğini manuel olarak bildirme ihtiyacını ortadan kaldırabilir, ancak yapıcının derleme zamanı güvenliğini kaybederiz. Herhangi bir zamanda biri yapıcıya başka bir bağımlılık eklerse, burada derleme zamanı hatasını almazdık. Bu, tespit edilmesi kolay olmayan testlerin başarısız olmasına neden olabilir. Örneği manuel olarak kurmak için @BeforeEach
kullanmayı tercih ederim:
@ExtendWith(MockitoExtension.class) class PersonGeneratorShould { @Sahte RandomGenerator randomGenerator; özel PersonGenerator üreteci; @BeforeEach geçersiz kurulum() { jeneratör = new PersonGenerator(randomGenerator); } @Ölçek geçersiz createValidPerson() { ne zaman(randomGenerator.generateRandomString()).thenReturn("John Doe"); ne zaman(randomGenerator.generateRandomInteger()).thenReturn(20); Kişi kişi = generator.generateRandom(); assertThat(person).isEqualTo(new Person("John Doe", 20)); } }
Zamana duyarlı süreçleri test etme
Bir kod parçası genellikle zaman damgalarına bağlıdır ve mevcut dönem zaman damgasını almak için System.currentTimeMillis()
gibi yöntemler kullanma eğilimindeyiz.
Bu iyi görünse de, sınıf bizim için dahili olarak kararlar verdiğinde kodumuzun doğru çalışıp çalışmadığını test etmek ve kanıtlamak zordur. Böyle bir kararın bir örneği, içinde bulunulan günün ne olduğunu belirlemek olabilir.
class IndexerShould { özel Dizin Oluşturucu dizinleyici = yeni Dizin Oluşturucu(); @Ölçek void createIndexNameForTomorrow() { String indexName = indexer.tomorrow("dizinim"); // bu test bugün işe yarar, peki ya yarın? assertThat(indexName) .isEqualTo("my-index.2022-02-02"); } }
Dizin adını oluştururken günün ne olduğunu 'kontrol edebilmek' için tekrar Bağımlılık Enjeksiyonu kullanmalıyız.
Java, bunun gibi kullanım durumlarını işlemek için bir Clock
sınıfına sahiptir. Zamanı kontrol etmek için bir Clock
örneğini Indexer
iletebiliriz. Varsayılan kurucu, geriye dönük uyumluluk için Clock.systemUTC()
kullanabilir. Artık System.currentTimeMillis()
çağrılarını clock.millis()
ile değiştirebiliriz.
Bir Clock
enjekte ederek, sınıflarımızda öngörülebilir bir süre uygulayabilir ve daha iyi testler yazabiliriz.
Dosya üretme yöntemlerini test etme
- Çıktılarını dosyalara yazan sınıfları nasıl test etmeliyiz?
- Herhangi bir işletim sisteminde çalışabilmeleri için bu dosyaları nerede saklamalıyız?
- Dosyanın zaten var olmadığından nasıl emin olabiliriz?
Dosyalarla uğraşırken, aşağıdaki örnekte göreceğimiz gibi, bu endişeleri kendimiz halletmeye çalışırsak, testler yazmak zor olabilir. Aşağıdaki test, kalitesi şüpheli eski bir testtir. DogToCsvWriter
köpekleri bir CSV dosyasına serileştirip yazıp yazmadığını test etmelidir:
sınıf DogToCsvWriterShould { private DogToCsvWriter yazar = new DogToCsvWriter("/tmp/dogs.csv"); @Ölçek geçersiz convertToCsv() { writer.appendAsCsv(new Dog(Breed.CORGI, Color.BROWN, "Monty")); writer.appendAsCsv(new Dog(Breed.MALTESE, Color.WHITE, "Zoe")); String csv = Files.readString("/tmp/dogs.csv"); assertThat(csv).isEqualTo("Monty,corgi,kahverengi\nZoe,malta dili,beyaz"); } }
Serileştirme süreci, yazma sürecinden ayrılmalıdır, ancak testi düzeltmeye odaklanalım.
Yukarıdaki testle ilgili ilk sorun, Windows kullanıcıları /tmp/dogs.csv
yolunu çözemeyeceğinden, Windows'ta çalışmamasıdır. Diğer bir sorun ise, yukarıdaki test yürütüldüğünde silinmediği için dosya zaten mevcutsa çalışmayacaktır. Bir CI/CD işlem hattında sorunsuz çalışabilir, ancak birden çok kez çalıştırılırsa yerel olarak çalışmayabilir.
JUnit 5, sizin için çerçeve tarafından oluşturulan ve silinen geçici bir dizine referans almak için kullanabileceğiniz bir ek açıklamaya sahiptir. Geçici dosyalar oluşturma ve silme mekanizması çerçeveden çerçeveye değişse de fikirler aynı kalır.
sınıf DogToCsvWriterShould { @Ölçek void convertToCsv(@TempDir Path tempDir) { Yol köpeklerCsv = tempDir.resolve("dogs.csv"); DogToCsvWriter yazar = yeni DogToCsvWriter(dogsCsv); writer.appendAsCsv(new Dog(Breed.CORGI, Color.BROWN, "Monty")); writer.appendAsCsv(new Dog(Breed.MALTESE, Color.WHITE, "Zoe")); String csv = Files.readString(dogsCsv); assertThat(csv).isEqualTo("Monty,corgi,kahverengi\nZoe,malta dili,beyaz"); } }
Bu küçük değişiklikle, yukarıdaki testin mutlak yollar hakkında endişelenmenize gerek kalmadan Windows, macOS ve Linux üzerinde çalışacağından eminiz. Ayrıca, oluşturulan dosyaları testten sonra silecektir, böylece artık birden çok kez çalıştırabilir ve her seferinde tahmin edilebilir sonuçlar alabiliriz.
Komuta karşı sorgu testi
Komut ve sorgu arasındaki fark nedir?
- Komut : bir nesneye, bir değer döndürmeden efekt üreten bir eylemi gerçekleştirmesi talimatını veririz (void yöntemleri)
- Sorgu : bir nesneden bir eylem gerçekleştirmesini ve bir sonuç veya istisna döndürmesini isteriz
Şimdiye kadar, esas olarak, eylem aşamasında bir değer döndüren veya bir istisna oluşturan bir yöntem çağırdığımız sorguları test ettik. void
yöntemlerini nasıl test edebilir ve diğer sınıflarla doğru etkileşime girip girmediklerini nasıl görebiliriz? Çerçeveler, bu tür testleri yazmak için farklı yöntemler sağlar.
Sorgular için şu ana kadar yazdığımız iddialar, assertThat
ile başlıyordu. Komut testleri yazarken, sorgularda yaptığımız gibi yöntemlerin doğrudan sonuçlarını incelemediğimiz için farklı bir yöntem kümesi kullanırız. Yöntemimizin sistemimizin diğer bölümleriyle olan etkileşimlerini 'doğrulamak' istiyoruz.
@ExtendWith(MockitoExtension.class) class FeedMentionServiceShould { @Sahte özel FeedRepository deposu; @Sahte özel FeedMentionEventEmitter emitörü; özel FeedMentionService hizmeti; @BeforeEach geçersiz kurulum() { hizmet = new FeedMentionService(depo, emitör); } @Ölçek void insertMentionToFeed() { uzun besleme kimliği = 1L; Mansiyon sözü = ...; ne zaman(repository.upsertMention(feedId, söz)) .thenReturn(UpsertResult.success(feedId, söz)); FeedInsertionEvent olayı = new FeedInsertionEvent(feedId, bahsetme); sözService.insertMentionToFeed(olay); doğrulamak(yayıcı).mentionInsertedToFeed(feedId, bahsetme); doğrulamaNoMoreInteractions(yayıcı); } }
Bu testte, feed'imizde belirtilmesi istendiğinde bir UpsertResult.success
ile yanıt vermesi için ilk önce havuzumuzla alay ettik. Burada depoyu test etmekle ilgilenmiyoruz. Depo yöntemleri FeedRepositoryShould
içinde test edilmelidir. Bu davranışla alay ederek, aslında depo yöntemini çağırmadık. Bir dahaki sefere çağrıldığında nasıl yanıt vereceğini söyledik.
Daha sonra söz konusu hizmetimize bu sözü mentionService
eklemesini söyledik. Sonucu yalnızca, sözü beslemeye başarıyla eklediyse yayınlaması gerektiğini biliyoruz. verify
yöntemini kullanarak, söz ve feed'imizle söz mentionInsertedToFeed
yönteminin çağrıldığından ve doğrulamaNoMoreInteractions kullanılarak yeniden çağrılmadığından verifyNoMoreInteractions
olabiliriz.
Son düşünceler
Kaliteli testler yazmak deneyimden gelir ve öğrenmenin en iyi yolu yapmaktır. Bu blogda yazılan ipuçları pratikten geliyor. Onlarla hiç karşılaşmadıysanız bazı tuzakları görmek zordur ve umarım bu öneriler kod tasarımınızı daha sağlam hale getirir. Güvenilir testlere sahip olmak, kodunuzu her dağıtmak zorunda kaldığınızda, hiç zorlanmadan bir şeyleri değiştirme konusundaki güveninizi artıracaktır.
Mediatoolkit ekibine katılmak ister misiniz?
Kıdemli Ön Uç Geliştirici için açık pozisyonumuza göz atın!