JUnit ile Sağlam Birim ve Entegrasyon Testleri Kılavuzu
Yayınlanan: 2022-03-11Otomatik yazılım testleri, yazılım projelerinin uzun vadeli kalitesi, sürdürülebilirliği ve genişletilebilirliği için kritik öneme sahiptir ve Java için JUnit, otomasyona giden yoldur.
Bu makalenin çoğu, sağlam birim testleri yazmaya ve saplama, alay etme ve bağımlılık eklemeyi kullanmaya odaklanacak olsa da, JUnit ve entegrasyon testlerini de tartışacağız.
JUnit test çerçevesi, Java tabanlı projeleri test etmek için yaygın, ücretsiz ve açık kaynaklı bir araçtır.
Bu yazı itibariyle, JUnit 4, 10 yıldan daha uzun bir süre önce piyasaya sürülen mevcut ana sürümdür ve son güncelleme iki yıldan daha uzun bir süre önce yapılmıştır.
JUnit 5 (Jüpiter programlama ve genişletme modelleri ile) aktif geliştirme aşamasındadır. Java 8'de tanıtılan dil özelliklerini daha iyi destekler ve diğer yeni, ilginç özellikleri içerir. Bazı takımlar JUnit 5'i kullanıma hazır bulabilirken, diğerleri JUnit 4'ü 5 resmi olarak yayınlanana kadar kullanmaya devam edebilir. Her ikisinden de örneklere bakacağız.
JUnit'i Çalıştırmak
JUnit testleri doğrudan IntelliJ'de çalıştırılabilir, ancak Eclipse, NetBeans ve hatta komut satırı gibi diğer IDE'lerde de çalıştırılabilirler.
Testler, özellikle birim testleri olmak üzere her zaman derleme zamanında çalıştırılmalıdır. Sorunun üretimde mi yoksa test kodunda mı olduğuna bakılmaksızın, başarısız testlere sahip bir derleme başarısız olarak kabul edilmelidir - bu, ekibin disiplinli olmasını ve başarısız olan testleri çözmeye en yüksek önceliği vermeye istekli olmasını gerektirir, ancak aşağıdakilere bağlı kalmak gerekir: otomasyon ruhu.
JUnit testleri, Jenkins gibi sürekli entegrasyon sistemleri tarafından da çalıştırılabilir ve raporlanabilir. Gradle, Maven veya Ant gibi araçları kullanan projeler, derleme sürecinin bir parçası olarak testler yapabilme avantajına sahiptir.
kepçe
JUnit 5 için örnek bir Gradle projesi olarak, JUnit kullanıcı kılavuzunun Gradle bölümüne ve junit5-samples.git deposuna bakın. JUnit 4 API ( "vintage" olarak anılır) kullanan testleri de çalıştırabileceğini unutmayın.
Proje, projeyi Gradle'dan içe aktarmak için Dosya > Aç… > junit-gradle-consumer sub-directory
gidin > Tamam > Proje Olarak Aç > Tamam menü seçeneği aracılığıyla IntelliJ'de oluşturulabilir.
Eclipse için, Buildship Gradle eklentisi Help > Eclipse Marketplace'ten yüklenebilir… Proje daha sonra File > Import… > Gradle > Gradle Project > Next > Next > junit-gradle-consumer
alt dizinine gözat > İleri ile içe aktarılabilir. > İleri > Bitir.
Gradle projesini IntelliJ veya Eclipse'de kurduktan sonra, Gradle build
görevinin çalıştırılması, test
göreviyle birlikte tüm JUnit testlerinin çalıştırılmasını içerecektir. Kodda herhangi bir değişiklik yapılmadıysa, sonraki build
yürütmelerinde testlerin atlanabileceğini unutmayın.
JUnit 4 için, JUnit'in Gradle wiki ile kullanımına bakın.
Uzman
JUnit 5 için, bir Maven projesi örneği için kullanıcı kılavuzunun Maven bölümüne ve junit5-samles.git deposuna bakın. Bu aynı zamanda eski testleri de çalıştırabilir (JUnit 4 API kullananlar).
IntelliJ'de Dosya > Aç… > junit-maven-consumer/pom.xml
> Tamam > Proje Olarak Aç'a gidin. Testler daha sonra Maven Projeleri > junit5-maven-consumer > Yaşam Döngüsü > Test'ten çalıştırılabilir.
Eclipse'de Dosya > İçe Aktar… > Maven > Mevcut Maven Projeleri > İleri > junit-maven-consumer
dizinine göz atın > pom.xml
seçiliyken > Bitir'i kullanın.
Testler, proje Maven build… > test
hedefini belirle > Çalıştır olarak çalıştırılarak yürütülebilir.
JUnit 4 için Maven deposundaki JUnit'e bakın.
Geliştirme Ortamları
Gradle veya Maven gibi derleme araçları aracılığıyla testler çalıştırmanın yanı sıra, birçok IDE doğrudan JUnit testlerini çalıştırabilir.
IntelliJ FİKİR
JUnit 5 testleri için IntelliJ IDEA 2016.2 veya üstü gerekirken, JUnit 4 testleri eski IntelliJ sürümlerinde çalışmalıdır.
Bu makalenin amaçları doğrultusunda, basit Person
sınıfı örneğindeki tüm dosyaları içeren GitHub depolarımdan birinden (JUnit5IntelliJ.git veya JUnit4IntelliJ.git) IntelliJ'de yeni bir proje oluşturmak ve yerleşik dosyayı kullanmak isteyebilirsiniz. JUnit kütüphaneleri. Test, Çalıştır > 'Tüm Testleri Çalıştır' seçeneği ile çalıştırılabilir. Test, PersonTest
sınıfından IntelliJ'de de çalıştırılabilir.
Bu depolar yeni IntelliJ Java projeleriyle oluşturuldu ve src/main/java/com/example
ve src/test/java/com/example
dizin yapılarını oluşturdu. src/main/java
dizini kaynak klasör olarak, src/test/java
ise test kaynak klasörü olarak belirtildi. PersonTest
sınıfını @Test
ile açıklamalı bir test yöntemiyle oluşturduktan sonra derleme başarısız olabilir, bu durumda IntelliJ, IntelliJ IDEA dağıtımından yüklenebilecek sınıf yoluna JUnit 4 veya JUnit 5 ekleme önerisi sunar (bkz. daha fazla ayrıntı için Yığın Taşması yanıtları). Son olarak, Tüm Testler için bir JUnit çalıştırma yapılandırması eklendi.
Ayrıca IntelliJ Testi Nasıl Yapılır Yönergelerine bakın.
tutulma
Eclipse'deki boş bir Java projesinin test kök dizini olmayacaktır. Bu, Özellikler > Java Yapı Yolu > Klasör Ekle… > Yeni Klasör Oluştur… > Klasör adını belirtin > Bitir'den eklenmiştir. Yeni dizin kaynak klasör olarak seçilecektir. Kalan her iki iletişim kutusunda da Tamam'ı tıklayın.
JUnit 4 testleri, Dosya > Yeni > JUnit Test Case ile oluşturulabilir. Testler için “New JUnit 4 testi” ve yeni oluşturulan kaynak klasörü seçin. Paketin test edilen sınıfla eşleştiğinden emin olarak bir "test edilen sınıf" ve bir "paket" belirtin. Ardından, test sınıfı için bir ad belirleyin. Sihirbazı bitirdikten sonra, istenirse, derleme yoluna "JUnit 4 kitaplığı ekle"yi seçin. Proje veya bireysel test sınıfı daha sonra bir JUnit Testi olarak çalıştırılabilir. Ayrıca bkz. Eclipse Yazma ve JUnit testlerini Çalıştırma.
NetBeans
NetBeans yalnızca JUnit 4 testlerini destekler. Test sınıfları bir NetBeans Java projesinde Dosya > Yeni Dosya… > Birim Testleri > JUnit Testi veya Mevcut Sınıf Testi ile oluşturulabilir. Varsayılan olarak, test kök dizini proje dizininde test
olarak adlandırılır.
Basit Üretim Sınıfı ve JUnit Test Senaryosu
Çok basit bir Person
sınıfı için basit bir üretim kodu örneğine ve buna karşılık gelen birim test koduna bir göz atalım. Örnek kodu github projemden indirebilir ve IntelliJ ile açabilirsiniz.
src/main/java/com/example/Person.java
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }
Değişmez Person
sınıfının bir yapıcısı ve bir getDisplayName()
yöntemi vardır. getDisplayName()
işlevinin beklediğimiz gibi biçimlendirilmiş adı döndürdüğünü test etmek istiyoruz. Tek bir birim testi için test kodu (JUnit 5):
src/test/java/com/example/PersonTest.java
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }
PersonTest
, JUnit 5'in @Test
ve iddiasını kullanır. JUnit 4 için, PersonTest
sınıfı ve yönteminin genel olması ve farklı içe aktarmaların kullanılması gerekir. İşte JUnit 4 örneği Gist.
IntelliJ'de PersonTest
sınıfı çalıştırıldığında, test başarılı olur ve UI göstergeleri yeşil olur.
Ortak JUnit Kuralları
adlandırma
Zorunlu olmasa da, test sınıfını adlandırırken ortak kuralları kullanıyoruz; özellikle, test edilen sınıfın adıyla başlıyoruz ( Person
) ve ona "Test" ekliyoruz ( PersonTest
). Test yöntemini adlandırmak, test edilen yöntemle ( getDisplayName( getDisplayName()
) başlayarak ve "test" öğesinin başına getirilerek ( testGetDisplayName()
) benzerdir. Test yöntemlerini adlandırmak için tamamen kabul edilebilir başka pek çok kural olsa da, ekip ve proje genelinde tutarlı olmak önemlidir.
Üretimdeki Adı | Testteki Ad |
---|---|
Kişi | Kişi Testi |
getDisplayName() | testDisplayName() |
paketler
Ayrıca, üretim kodunun Person
sınıfı ile aynı pakette ( com.example
) PersonTest
sınıfı test kodu oluşturma kuralını da kullanıyoruz. Testler için farklı bir paket kullansaydık, uygun olmasa bile üretim kodu sınıflarında, yapıcılarda ve birim testleri tarafından başvurulan yöntemlerde genel erişim değiştiricisini kullanmamız gerekir, bu nedenle bunları aynı pakette tutmak daha iyidir. . Ancak, genellikle yayınlanmış üretim yapılarına test kodunu dahil etmek istemediğimizden ayrı kaynak dizinleri ( src/main/java
ve src/test/java
) kullanıyoruz.
Yapı ve Açıklama
@Test
ek açıklaması (JUnit 4/5), testGetDisplayName()
yöntemini bir test yöntemi olarak yürütmesini ve başarılı veya başarısız olup olmadığını bildirmesini söyler. Tüm iddialar (varsa) başarılı olduğu ve hiçbir istisna atılmadığı sürece, test başarılı kabul edilir.
Test kodumuz Arrange-Act-Assert (AAA) yapı modelini takip eder. Diğer yaygın kalıplar arasında Ne Zaman-Olur ve Kurulum-Uygulama-Doğrula-Teardown bulunur (Teardown genellikle birim testleri için açıkça gerekli değildir), ancak bu makalede AAA kullanıyoruz.
Test örneğimizin AAA'yı nasıl takip ettiğine bir göz atalım. İlk satır olan "arrange", test edilecek bir Person
nesnesi oluşturur:
Person person = new Person("Josh", "Hayden");
İkinci satır, "hareket", üretim kodunun Person.getDisplayName Person.getDisplayName()
yöntemini uygular:
String displayName = person.getDisplayName();
Üçüncü satır, "iddia", sonucun beklendiği gibi olduğunu doğrular.
assertEquals("Hayden, Josh", displayName);
Dahili olarak, assertEquals()
çağrısı, üretim kodu ( displayName
) eşleşmelerinden döndürülen gerçek değeri doğrulamak için “Hayden, Josh” String nesnesinin eşittir yöntemini kullanır. Eşleşmemiş olsaydı, test başarısız olarak işaretlenirdi.
Testlerin genellikle bu AAA aşamalarının her biri için birden fazla satırı olduğunu unutmayın.
Birim Testleri ve Üretim Kodu
Artık bazı test kurallarını ele aldığımıza göre, şimdi dikkatimizi üretim kodunu test edilebilir hale getirmeye çevirelim.
Bir kişinin doğum tarihine göre yaşını döndürmek için bir yöntem uyguladığım Person
sınıfımıza dönüyoruz. Kod örnekleri, Java 8'in yeni tarih ve işlevsel API'lerden yararlanmasını gerektirir. Yeni Person.java
sınıfı şöyle görünür:
kişi.java
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
Bu sınıfı çalıştırmak (yazarken) Joey'nin 4 yaşında olduğunu duyurur. Bir test yöntemi ekleyelim:
Kişi Testi.java
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }
Bugün geçiyor, peki ya bundan bir yıl sonra çalıştırdığımızda? Beklenen sonuç testi çalıştıran sistemin mevcut tarihine bağlı olduğundan, bu test deterministik değildir ve kırılgandır.
Bir Değer Tedarikçisini Saplamak ve Enjekte Etmek
Üretimde çalışırken, kişinin yaşını hesaplamak için geçerli tarihi LocalDate.now()
kullanmak istiyoruz, ancak bundan bir yıl sonra bile deterministik bir test yapmak için testlerin kendi currentDate
değerlerini sağlaması gerekiyor.
Bu, bağımlılık enjeksiyonu olarak bilinir. Person
nesnemizin mevcut tarihin kendisini belirlemesini istemiyoruz, bunun yerine bu mantığı bir bağımlılık olarak iletmek istiyoruz. Birim testleri bilinen, saplanmış bir değer kullanacak ve üretim kodu, çalışma zamanında sistem tarafından gerçek değerin sağlanmasına izin verecektir.
Person.java
bir LocalDate
tedarikçisi ekleyelim:
kişi.java
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
getAge()
yöntemini test etmeyi kolaylaştırmak için, geçerli tarihi almak için bir LocalDate
tedarikçisi olan currentDateSupplier
kullanmak üzere değiştirdik. Bir tedarikçinin ne olduğunu bilmiyorsanız, Lambda Yerleşik İşlevsel Arayüzler hakkında okumanızı tavsiye ederim.
Ayrıca bir bağımlılık enjeksiyonu ekledik: Yeni test oluşturucu, testlerin kendi güncel tarih değerlerini sağlamasına izin verir. Orijinal kurucu, bir LocalDate
nesnesi sağlayan LocalDate::now
statik bir yöntem referansını ileterek bu yeni kurucuyu çağırır, bu nedenle ana yöntemimiz hala eskisi gibi çalışır. Peki ya test yöntemimiz? PersonTest.java
güncelleyelim:
Kişi Testi.java
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
Test şimdi kendi currentDate
değerini enjekte ediyor, bu nedenle testimiz gelecek yıl veya herhangi bir yıl boyunca çalıştırıldığında yine de geçecek. Bu genellikle stubbing veya döndürülecek bilinen bir değer sağlanması olarak adlandırılır, ancak bu bağımlılığın enjekte edilmesine izin vermek için önce Person
değiştirmemiz gerekiyordu.
Person
nesnesini oluştururken lambda sözdizimine ( ()->currentDate
) dikkat edin. Bu, yeni oluşturucunun gerektirdiği şekilde LocalDate
tedarikçisi olarak değerlendirilir.
Bir Web Hizmetini Alay Etmek ve Saplamak
Tüm varlığı JVM belleğinde olan Person
nesnemizin dış dünya ile iletişim kurması için hazırız. İki yöntem eklemek istiyoruz: kişinin şu anki yaşını gönderecek publishAge()
getThoseInCommon()
yöntemi ve aynı doğum gününü paylaşan veya aynı yaşta olan ünlü kişilerin adlarını döndürecek getThoseInCommon() yöntemi Person
. "Kişilerin Doğum Günleri" adlı etkileşim kurabileceğimiz bir RESTful hizmeti olduğunu varsayalım. Bunun için tek sınıftan oluşan bir Java istemcimiz var, BirthdaysClient
.
com.example.birthdays.BirthdaysClient
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
Person
sınıfımızı geliştirelim. İstenen publishAge()
davranışı için yeni bir test yöntemi ekleyerek başlıyoruz. İşlevsellik yerine neden testle başlayasınız? Test odaklı geliştirme (TDD olarak da bilinir) ilkelerini takip ediyoruz, burada önce testi, sonra da geçmesini sağlayacak kodu yazıyoruz.
Kişi Testi.java
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }
Bu noktada, test kodu derlenemez çünkü publishAge()
yöntemini oluşturmadık. Boş bir Person.publishAge()
yöntemi oluşturduğumuzda her şey geçer. Artık, kişinin yaşının gerçekten BirthdaysClient
yayınlandığını doğrulamak için yapılan teste hazırız.

Alaylı Nesne Ekleme
Bu bir birim testi olduğundan, hızlı ve bellekte çalışması gerekir, bu nedenle test, Person
nesnemizi sahte bir BirthdaysClient
ile oluşturacak ve böylece aslında bir web isteğinde bulunmayacaktır. Test, beklendiği gibi çağrıldığını doğrulamak için bu sahte nesneyi kullanır. Bunu yapmak için, sahte nesneler oluşturmak için Mockito çerçevesine (MIT lisansı) bir bağımlılık ekleyeceğiz ve ardından alaylı bir BirthdaysClient
nesnesi oluşturacağız:
Kişi Testi.java
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
Ayrıca, bir BirthdaysClient
nesnesi almak için Person
yapıcısının imzasını artırdık ve alaylı BirthdaysClient
nesnesini enjekte etmek için testi değiştirdik.
Sahte Beklenti Ekleme
Ardından, testPublishAge
sonuna BirthdaysClient
çağrıldığı beklentisini ekleriz. Yeni PersonTest.java
gösterildiği gibi, Person.publishAge()
onu çağırmalıdır:
Kişi Testi.java
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }
Mockito ile geliştirilmiş BirthdaysClient
, yöntemlerine yapılan tüm çağrıların kaydını tutar; bu şekilde, yayınAge() çağrılmadan önce verifyZeroInteractions()
yöntemiyle BirthdaysClient
çağrı publishAge()
. Muhtemelen gerekli olmasa da, bunu yaparak yapıcının herhangi bir sahte arama yapmamasını sağlıyoruz. verify()
satırında, BirthdaysClient
çağrısının nasıl görüneceğini beklediğimizi belirtiriz.
PublishRegularPersonAge imzasında IOException olduğundan, bunu test yöntemi imzamıza da eklediğimizi unutmayın.
Bu noktada, test başarısız olur:
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
Test odaklı geliştirmeyi takip ettiğimiz için Person.java
için gerekli değişiklikleri henüz uygulamadığımız için bu beklenen bir durumdur. Şimdi gerekli değişiklikleri yaparak bu testi geçeceğiz:
kişi.java
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
İstisnaları Test Etme
Üretim kodu oluşturucusunun yeni bir BirthdaysClient
örneğini oluşturmasını sağladık ve publishAge()
şimdi birthdaysClient
öğesini çağırıyor. Tüm testler geçer; her şey yeşil. Harika! Ancak publishAge()
öğesinin IOException'ı yuttuğuna dikkat edin. Kabarcık yapmasına izin vermek yerine, onu PersonException.java adlı yeni bir dosyada kendi PersonException.java
ile sarmak istiyoruz:
PersonException.java
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
Bu senaryoyu PersonTest.java
yeni bir test yöntemi olarak uyguluyoruz:
Kişi Testi.java
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }
Mockito doThrow()
çağrısı, publishRegularPersonAge()
yöntemi çağrıldığında bir özel durum oluşturmak için birthdaysClient
saplamalar. PersonException
, testte başarısız oluruz. Aksi takdirde, istisnanın IOException ile düzgün bir şekilde zincirlendiğini iddia eder ve istisna mesajının beklendiği gibi olduğunu doğrularız. Şu anda, üretim kodumuzda herhangi bir işleme uygulamadığımız için, beklenen istisna atılmadığından testimiz başarısız oluyor. Testi geçmek için Person.java
değiştirmemiz gerekenler:
kişi.java
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }
Taslaklar: Ne Zaman ve İddialar
Şimdi, Person.getThoseInCommon()
yöntemini uygulayarak, Person.Java
sınıfımızı böyle gösteriyoruz.
testGetThoseInCommon()
, testPublishAge() 'den farklı olarak, birthdaysClient
testPublishAge()
yöntemlerine belirli çağrıların yapıldığını doğrulamaz. Bunun yerine, getThoseInCommon() öğesinin yapması gereken getThoseInCommon()
findFamousNamesOfAge()
ve findFamousNamesBornOn()
çağrıları için when
saplama çağrıları döndürdüğünü kullanır. Daha sonra sağladığımız inatçı isimlerin üçünün de döndürüldüğünü iddia ediyoruz.
assertAll()
JUnit 5 yöntemiyle birden çok onaylamayı sarmak, ilk başarısız onaylamadan sonra durmak yerine tüm onaylamaların bir bütün olarak denetlenmesine olanak tanır. Ayrıca, dahil edilmeyen belirli adları belirlemek için assertTrue()
içeren bir mesaj da ekledik. "Mutlu yol" (ideal bir senaryo) test yöntemimiz şöyle görünür (not edin, bu "mutlu yol" olmanın doğası gereği sağlam bir test dizisi değildir, ancak nedenini daha sonra konuşacağız.
Kişi Testi.java
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }
Test Kodunu Temiz Tutun
Sıklıkla gözden kaçırılsa da, test kodunu iltihaplı tekrarlardan uzak tutmak da aynı derecede önemlidir. Temiz kod ve “kendini tekrar etme” gibi ilkeler, yüksek kaliteli bir kod tabanı, üretim ve test kodu sağlamak için çok önemlidir. Birkaç test yöntemimiz olduğuna göre, en son PersonTest.java'nın bazı yinelemelere sahip olduğuna dikkat edin.
Bunu düzeltmek için birkaç şey yapabiliriz:
IOException nesnesini özel bir son alana çıkarın.
Person nesnelerinin çoğu aynı parametrelerle oluşturulduğundan,
Person
nesnesi oluşturmayı kendi yöntemine (bu durumdacreateJoeSixteenJan2()
.Atılan
PersonExceptions
doğrulayan çeşitli testler için birassertCauseAndMessage()
oluşturun.
Temiz kod sonuçları, PersonTest.java dosyasının bu yorumunda görülebilir.
Mutlu Yoldan Daha Fazlasını Test Edin
Bir Person
nesnesinin doğum tarihi geçerli tarihten sonraysa ne yapmalıyız? Uygulamalardaki kusurlar genellikle beklenmedik girdilerden veya köşe, kenar veya sınır durumlarında öngörü eksikliğinden kaynaklanır. Bu durumları elimizden geldiğince tahmin etmeye çalışmak önemlidir ve birim testleri genellikle bunu yapmak için uygun bir yerdir. Person
ve PersonTest
, beklenen istisnalar için birkaç test ekledik, ancak bu hiçbir şekilde tamamlanmış değildi. Örneğin, saat dilimi verilerini temsil etmeyen veya depolamayan LocalDate
kullanıyoruz. Ancak LocalDate.now()
çağrılarımız, sistem kullanıcısınınkinden bir gün önce veya sonra olabilen, sistemin varsayılan saat dilimini temel alan bir LocalDate
döndürür. Uygulanan uygun testler ve davranışlarla bu faktörler dikkate alınmalıdır.
Sınırlar da test edilmelidir. getDaysUntilBirthday()
yöntemiyle bir Person
nesnesi düşünün. Test, kişinin doğum gününün içinde bulunulan yıl içinde geçip geçmediğini, kişinin doğum gününün bugün olup olmadığını ve artık yılın gün sayısını nasıl etkilediğini içermelidir. Bu senaryolar, kişinin doğum gününden bir gün önce, kişinin doğum gününden bir gün önce ve sonraki yılın artık yıl olduğu doğum gününden bir gün sonra kontrol edilerek ele alınabilir. İşte ilgili test kodu:
Kişi Testi.java
// ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
Entegrasyon Testleri
Çoğunlukla birim testlerine odaklandık, ancak JUnit entegrasyon, kabul, işlevsel ve sistem testleri için de kullanılabilir. Bu tür testler genellikle daha fazla kurulum kodu gerektirir, örneğin sunucuların başlatılması, bilinen verilerle veritabanlarının yüklenmesi vb. Çoğu zaman binlerce birim testini saniyeler içinde çalıştırabilsek de, büyük entegrasyon test takımlarının çalışması dakikalar hatta saatler alabilir. Entegrasyon testleri, genellikle kod boyunca her permütasyonu veya yolu kapsamaya çalışmak için kullanılmamalıdır; birim testleri bunun için daha uygundur.
Form doldurma, düğmelere tıklama, içeriğin yüklenmesini bekleme vb. işlemlerde web tarayıcılarını çalıştıran web uygulamaları için testler oluşturmak, genellikle 'Sayfa Nesne Kalıbı' ile birleştirilmiş Selenium WebDriver (Apache 2.0 lisansı) kullanılarak yapılır (bkz. SeleniumHQ github wiki ve Martin Fowler'ın Sayfa Nesneleri hakkındaki makalesi).
JUnit, Apache HTTP İstemcisi veya Spring Rest Template gibi bir HTTP istemcisinin kullanımıyla RESTful API'leri test etmek için etkilidir (HowToDoInJava.com iyi bir örnek sağlar).
Person
nesnesiyle ilgili durumumuzda, bir entegrasyon testi, People Birthdays hizmetinin temel URL'sini belirten bir yapılandırmayla, sahte olandan ziyade gerçek BirthdaysClient
kullanılmasını içerebilir. Bir entegrasyon testi daha sonra böyle bir hizmetin test örneğini kullanır, doğum günlerinin kendisine yayınlandığını doğrular ve hizmette iade edilecek ünlü kişiler yaratır.
Diğer JUnit Özellikleri
JUnit, örneklerde henüz keşfetmediğimiz birçok ek özelliğe sahiptir. Bazılarını açıklayacağız ve diğerleri için referanslar sağlayacağız.
Test Fikstürleri
JUnit'in her @Test
yöntemini çalıştırmak için test sınıfının yeni bir örneğini oluşturduğuna dikkat edilmelidir. JUnit ayrıca @Test
yöntemlerinin hepsinden veya her birinden önce veya sonra belirli yöntemleri çalıştırmak için açıklama kancaları sağlar. Bu kancalar genellikle veritabanını veya sahte nesneleri ayarlamak veya temizlemek için kullanılır ve JUnit 4 ile 5 arasında farklılık gösterir.
JÜnite 4 | JBirim 5 | Statik Yöntem için mi? |
---|---|---|
@BeforeClass | @BeforeAll | Evet |
@AfterClass | @AfterAll | Evet |
@Before | @BeforeEach | Numara |
@After | @AfterEach | Numara |
PersonTest
örneğimizde, BirthdaysClient
sahte nesnesini @Test
yöntemlerinin kendisinde yapılandırmayı seçtik, ancak bazen birden çok nesne içeren daha karmaşık sahte yapıların oluşturulması gerekir. @BeforeEach
(JUnit 5'te) ve @Before
(JUnit 4'te) genellikle bunun için uygundur.
@After*
ek açıklamaları, JVM çöp toplama birimi birim testleri için oluşturulan çoğu nesneyi işlediğinden, entegrasyon testlerinde birim testlerinden daha yaygındır. @BeforeClass
ve @BeforeAll
ek açıklamaları en yaygın olarak, her bir test yöntemi için değil, maliyetli kurulum ve sökme işlemlerini bir kez gerçekleştirmesi gereken entegrasyon testleri için kullanılır.
JUnit 4 için lütfen test fikstürleri kılavuzuna bakın (genel konseptler JUnit 5 için hala geçerlidir).
Test Takımları
Bazen birden fazla ilgili testi çalıştırmak istersiniz, ancak tüm testleri değil. Bu durumda, test grupları, test takımları halinde oluşturulabilir. Bunu JUnit 5'te nasıl yapacağınızı öğrenmek için HowToProgram.xyz'in JUnit 5 makalesine ve JUnit ekibinin JUnit 4 belgelerine bakın.
JUnit 5'in @Nested ve @DisplayName
JUnit 5, testler arasındaki ilişkiyi daha iyi göstermek için statik olmayan iç içe sınıfları kullanma yeteneği ekler. Bu, JavaScript için Jasmine gibi test çerçevelerinde iç içe tanımlarla çalışmış olanlara çok aşina olmalıdır. Bunu kullanmak için iç sınıflar @Nested
ile açıklanmıştır.
@DisplayName
ek açıklaması da JUnit 5'te yenidir ve test yöntemi tanımlayıcısına ek olarak gösterilecek dize biçiminde raporlama için testi tanımlamanıza olanak tanır.
@Nested
ve @DisplayName
birbirinden bağımsız olarak kullanılabilse de, birlikte sistemin davranışını tanımlayan daha net test sonuçları sağlayabilirler.
Hamcrest Eşleştiriciler
Hamcrest çerçevesi, kendisi JUnit kod tabanının bir parçası olmasa da, testlerde geleneksel iddia yöntemlerini kullanmaya bir alternatif sunarak daha anlamlı ve okunabilir test koduna izin verir. Hem geleneksel assertEquals hem de Hamcrest assertThat kullanarak aşağıdaki doğrulamaya bakın:
//Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));
Hamcrest, hem JUnit 4 hem de 5 ile kullanılabilir. Vogella.com'un Hamcrest hakkındaki öğreticisi oldukça kapsamlıdır.
Ek kaynaklar
Birim Testleri, Test Edilebilir Kod Nasıl Yazılır ve Neden Önemlidir makalesi, temiz, test edilebilir kod yazmanın daha spesifik örneklerini kapsar.
Güvenle Oluşturun: JUnit Testlerine Yönelik Bir Kılavuz, birim ve entegrasyon testlerine yönelik farklı yaklaşımları ve neden bir tanesini seçip ona bağlı kalmanın en iyisi olduğunu inceler.
JUnit 4 Wiki ve JUnit 5 Kullanım Kılavuzu her zaman mükemmel bir referans noktasıdır.
Mockito belgeleri, ek işlevler ve örnekler hakkında bilgi sağlar.
JUnit, Otomasyona Giden Yoldur
JUnit ile Java dünyasında test etmenin birçok yönünü araştırdık. Java kod tabanları için JUnit çerçevesini kullanan birim ve entegrasyon testlerine, JUnit'i geliştirme ve oluşturma ortamlarına entegre etmeye, tedarikçiler ve Mockito ile maketlerin ve taslakların nasıl kullanılacağına, ortak sözleşmelere ve en iyi kod uygulamalarına, neyin test edileceğine ve bunlardan bazılarına baktık. diğer harika JUnit özellikleri.
Şimdi JUnit çerçevesini kullanarak otomatik testlerin faydalarını ustaca uygulama, sürdürme ve toplama konusunda büyüme sırası okuyucuda.