JUnit ile Sağlam Birim ve Entegrasyon Testleri Kılavuzu

Yayınlanan: 2022-03-11

Otomatik 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.

Uyumluluğu gösteren dişli grupları: birinde NetBeans ile JUnit 4, diğerinde Eclipse ve Gradle ile JUnit 5 ve sonuncusu Maven ve IntelliJ IDEA ile JUnit 5'e sahip.

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 durumda createJoeSixteenJan2() .

  • Atılan PersonExceptions doğrulayan çeşitli testler için bir assertCauseAndMessage() 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.