Bir Ünite Testi Uygulayıcısının Günlük Mockito Rehberi
Yayınlanan: 2022-03-11Çevik çağında birim testi zorunlu hale geldi ve otomatik teste yardımcı olacak birçok araç var. Böyle bir araç, testler için alaylı nesneler oluşturmanıza ve yapılandırmanıza izin veren açık kaynaklı bir çerçeve olan Mockito'dur.
Bu makalede, test edilen sistemin beklenen davranışını doğrulamak için sahte oluşturma ve yapılandırmayı ve bunları kullanmayı ele alacağız. Ayrıca tasarımını ve uyarılarını daha iyi anlamak için Mockito'nun içindekilere biraz dalacağız. JUnit'i birim test çerçevesi olarak kullanacağız, ancak Mockito JUnit'e bağlı olmadığından, farklı bir çerçeve kullanıyor olsanız bile takip edebilirsiniz.
Mockito Edinme
Mockito almak bu günlerde kolay. Gradle kullanıyorsanız, bu tek satırı derleme komut dosyanıza eklemeniz gerekir:
testCompile "org.mockito:mockito−core:2.7.7"
Benim gibi hala Maven'i tercih edenlere gelince, Mockito'yu şu şekilde bağımlılıklarınıza ekleyin:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
Elbette dünya Maven ve Gradle'dan çok daha geniş. Mockito jar yapısını Maven merkezi deposundan getirmek için herhangi bir proje yönetim aracını kullanmakta özgürsünüz.
Yaklaşan Mockito
Birim testleri, bağımlılıklarının davranışına dayanmadan belirli sınıfların veya yöntemlerin davranışını test etmek için tasarlanmıştır. En küçük kod 'birimini' test ettiğimiz için, bu bağımlılıkların gerçek uygulamalarını kullanmamıza gerek yok. Ayrıca, farklı davranışları test ederken bu bağımlılıkların biraz farklı uygulamalarını kullanacağız. Buna geleneksel, iyi bilinen bir yaklaşım, belirli bir senaryoya uygun bir arabirimin özel uygulamaları olan 'saplamalar' oluşturmaktır. Bu tür uygulamalar genellikle sabit kodlanmış bir mantığa sahiptir. Saplama, bir tür test dublörüdür. Diğer türler arasında sahteler, alaylar, casuslar, aptallar vb.
Mockito tarafından yoğun olarak kullanıldığı için, yalnızca iki tür test dublörü, 'alay' ve 'casus' üzerinde odaklanacağız.
alaylar
alay etmek nedir? Açıkçası, geliştirici arkadaşlarınızla dalga geçtiğiniz yer burası değil. Birim testi için alay, gerçek bir alt sistemin davranışını kontrollü yollarla uygulayan bir nesne yarattığınız zamandır. Kısacası, alaylar bir bağımlılığın yerine kullanılır.
Mockito ile bir taklit oluşturursunuz, Mockito'ya belirli yöntemler çağrıldığında ne yapacağını söylersiniz ve ardından testinizde gerçek şey yerine sahte örneği kullanırsınız. Testten sonra, hangi belirli yöntemlerin çağrıldığını görmek için alayı sorgulayabilir veya yan etkileri değişen durum şeklinde kontrol edebilirsiniz.
Varsayılan olarak, Mockito, sahtenin her yöntemi için bir uygulama sağlar.
casuslar
Casus, Mockito'nun yarattığı diğer test dublörü türüdür. Sahtelerin aksine, bir casus oluşturmak için casusluk yapmak için bir örnek gerekir. Varsayılan olarak, bir casus tüm yöntem çağrılarını gerçek nesneye devreder ve hangi yöntemin ve hangi parametrelerle çağrıldığını kaydeder. Onu casus yapan da bu: Gerçek bir nesneyi gözetliyor.
Mümkün olduğunda casuslar yerine sahte kullanmayı düşünün. Casuslar, kolayca test edilebilecek şekilde yeniden tasarlanamayan eski kodları test etmek için faydalı olabilir, ancak bir sınıfla kısmen alay etmek için bir casus kullanma ihtiyacı, bir sınıfın çok fazla şey yaptığını ve dolayısıyla tek sorumluluk ilkesini ihlal ettiğinin bir göstergesidir.
Basit Bir Örnek Oluşturma
Testlerini yazabileceğimiz basit bir demoya bakalım. Bir kullanıcıyı tanımlayıcısına göre bulmak için tek bir yöntemle bir UserRepository
arabirimimiz olduğunu varsayalım. Ayrıca düz metin parolayı parola karmasına dönüştürmek için bir parola kodlayıcı konseptimiz var. Hem UserRepository
hem de PasswordEncoder
, yapıcı aracılığıyla UserService
bağımlılıklarıdır (işbirlikçiler olarak da adlandırılır). Demo kodumuz şöyle görünür:
Kullanıcı Deposu
public interface UserRepository { User findById(String id); }
kullanıcı
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
Şifre Kodlayıcı
public interface PasswordEncoder { String encode(String password); }
Kullanıcı Hizmeti
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
Bu örnek kod GitHub'da bulunabilir, böylece bu makalenin yanında incelemek üzere indirebilirsiniz.
Mockito'yu Uygulamak
Örnek kodumuzu kullanarak Mockito'nun nasıl uygulanacağına bakalım ve bazı testler yazalım.
Mock Oluşturma
Mockito ile sahte oluşturmak, Mockito.mock()
statik yöntemini çağırmak kadar kolaydır:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Mockito için statik içe aktarmaya dikkat edin. Bu makalenin geri kalanında, dolaylı olarak bu içe aktarmanın eklendiğini ele alacağız.
İçe aktarma işleminden sonra, bir arayüz olan PasswordEncoder
ile alay ediyoruz. Mockito, yalnızca arayüzlerle değil, aynı zamanda soyut sınıflarla ve nihai olmayan somut sınıflarla da alay eder. Kullanıma hazır, Mockito, son sınıflar ve nihai veya statik yöntemlerle alay edemez, ancak gerçekten ihtiyacınız varsa, Mockito 2, deneysel MockMaker eklentisini sağlar.
Ayrıca equals()
ve hashCode()
yöntemlerinin taklit edilemeyeceğini unutmayın.
Casuslar Yaratmak
Bir casus oluşturmak için Mockito'nun statik yöntemini spy()
çağırmanız ve casusluk için bir örnek iletmeniz gerekir. Döndürülen nesnenin çağrı yöntemleri, bu yöntemler saplanmadıkça gerçek yöntemleri çağıracaktır. Bu çağrılar kaydedilir ve bu çağrıların gerçekleri doğrulanabilir ( verify()
ile ilgili daha fazla açıklamaya bakın). Hadi bir casus yapalım:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
Bir casus yaratmak, bir sahtekar yaratmaktan pek farklı değildir. Ayrıca, bir sahte yapılandırma için kullanılan tüm Mockito yöntemleri, bir casus yapılandırmaya da uygulanabilir.
Casuslar, taklitlere kıyasla nadiren kullanılır, ancak bunları, kısmen alay etmeyi gerektiren, yeniden düzenlenemeyen eski kodları test etmek için yararlı bulabilirsiniz. Bu gibi durumlarda, istediğiniz davranışı elde etmek için bir casus oluşturabilir ve bazı yöntemlerini saplayabilirsiniz.
Varsayılan Dönüş Değerleri
mock(PasswordEncoder.class)
çağrılması, PasswordEncoder
örneğini döndürür. Yöntemlerini bile çağırabiliriz, ama ne döndürecekler? Varsayılan olarak, tüm sahte yöntemler "başlatılmamış" veya "boş" değerler döndürür, örneğin sayısal türler (hem ilkel hem de kutulu) için sıfırlar, booleanlar için false ve diğer çoğu tür için boş değerler.
Aşağıdaki arayüzü göz önünde bulundurun:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
Şimdi, bir sahte yöntemden hangi varsayılan değerlerin bekleneceği hakkında bir fikir veren aşağıdaki parçacığı göz önünde bulundurun:
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
Saplama Yöntemleri
Taze, değiştirilmemiş taklitler yalnızca nadir durumlarda faydalıdır. Genellikle, alayı yapılandırmak ve alayın belirli yöntemleri çağrıldığında ne yapılacağını tanımlamak istiyoruz. Buna saplama denir.
Mockito, saplamanın iki yolunu sunar. İlk yol “bu yöntem çağrıldığında bir şeyler yap” şeklindedir. Aşağıdaki parçacığı göz önünde bulundurun:
when(passwordEncoder.encode("1")).thenReturn("a");
Neredeyse İngilizce gibi okunur: “ passwordEncoder.encode(“1”)
çağrıldığında, bir a
döndürün.”
Saplamanın ikinci yolu, daha çok "Bu sahtenin yöntemi aşağıdaki argümanlarla çağrıldığında bir şeyler yap" şeklindedir. Nedeni sonunda belirtildiğinden, bu saplama şeklinin okunması daha zordur. Düşünmek:
doReturn("a").when(passwordEncoder).encode("1");
Bu saplama yöntemine sahip snippet şöyle olur: " passwordEncoder
encode()
yöntemi 1
bağımsız değişkeni ile çağrıldığında a
döndür."
Birinci yol, typesafe olması ve daha okunaklı olması nedeniyle tercih edilir. Ancak nadiren, örneğin gerçek bir casus yöntemini saplarken olduğu gibi, ikinci yolu kullanmak zorunda kalırsınız çünkü onu çağırmanın istenmeyen yan etkileri olabilir.
Mockito tarafından sağlanan saplama yöntemlerini kısaca inceleyelim. Örneklerimize saplamanın her iki yolunu da dahil edeceğiz.
Dönen Değerler
thenReturn
veya doReturn()
, yöntem çağrıldığında döndürülecek bir değeri belirtmek için kullanılır.
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
veya
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
Ardışık yöntem çağrılarının sonuçları olarak döndürülecek birden çok değer de belirtebilirsiniz. Son değer, diğer tüm yöntem çağrıları için sonuç olarak kullanılacaktır.
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
veya
//do doReturn("a", "b").when(passwordEncoder).encode("1");
Aynı şey aşağıdaki snippet ile de elde edilebilir:
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
Bu model, ardışık çağrıların sonuçlarını tanımlamak için diğer saplama yöntemleriyle de kullanılabilir.
Özel Yanıtları Döndürme
then()
, thenAnswer() için bir takma ad ve thenAnswer()
, bir yöntem çağrıldığında döndürülecek özel bir yanıt ayarlayan aynı şeyi doAnswer()
, şöyle:
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
veya
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
thenAnswer()
'ın aldığı tek argüman, Answer
arayüzünün bir uygulamasıdır. InvocationOnMock
türünde bir parametreye sahip tek bir yöntemi vardır.
Ayrıca bir yöntem çağrısının sonucu olarak bir istisna atabilirsiniz:
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
…veya bir sınıfın gerçek yöntemini çağırın (arayüzlere uygulanamaz):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
Hantal göründüğünü düşünüyorsanız haklısınız. Mockito, testinizin bu yönünü kolaylaştırmak için thenCallRealMethod()
ve thenThrow()
öğelerini sağlar.
Gerçek Yöntemleri Çağırma
Adından da anlaşılacağı gibi, thenCallRealMethod()
ve doCallRealMethod()
sahte bir nesnede gerçek yöntemi çağırır:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
Kısmi taklitlerde gerçek yöntemleri çağırmak faydalı olabilir, ancak çağrılan yöntemin istenmeyen yan etkileri olmadığından ve nesne durumuna bağlı olmadığından emin olun. Eğer öyleyse, bir casus sahtekardan daha uygun olabilir.
Bir arabirim alayı oluşturur ve gerçek bir yöntemi çağırmak için bir saplama yapılandırmaya çalışırsanız, Mockito çok bilgilendirici mesajla bir istisna atar. Aşağıdaki parçacığı göz önünde bulundurun:
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockito aşağıdaki mesajla başarısız olur:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
Mockito geliştiricilerine bu kadar kapsamlı açıklamalar sağlamak için yeterince özen gösterdikleri için teşekkür ederiz!
İstisnaları Atma
thenThrow()
ve doThrow()
, bir istisna atmak için alaylı bir yöntem yapılandırır:
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
veya
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito, atılan istisnanın o belirli saplanmış yöntem için geçerli olmasını sağlar ve istisna, yöntemin kontrol edilen istisnalar listesinde değilse şikayet eder. Aşağıdakileri göz önünde bulundur:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
Bir hataya yol açacaktır:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
Gördüğünüz gibi Mockito, encode()
öğesinin bir IOException
oluşturamayacağını algıladı.
Bir istisna örneğini geçmek yerine bir istisna sınıfını da iletebilirsiniz:
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
veya
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
Bununla birlikte, Mockito, bir istisna örneğini doğrulayacağı şekilde bir istisna sınıfını doğrulayamaz, bu nedenle disiplinli olmanız ve yasadışı sınıf nesnelerini geçmemeniz gerekir. Örneğin, encode()
öğesinin işaretli bir istisna oluşturması beklenmese de, aşağıdakiler IOException
atar:
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
Varsayılan Yöntemlerle Alay Arayüzleri
Mockito'nun bir arayüz için bir sahte oluştururken o arayüzün tüm yöntemleriyle alay ettiğini belirtmekte fayda var. Java 8'den bu yana, arayüzler soyut olanlarla birlikte varsayılan yöntemler içerebilir. Bu yöntemler de alay konusu olduğu için varsayılan yöntemler gibi davranmalarına özen göstermeniz gerekir.
Aşağıdaki örneği göz önünde bulundurun:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
Bu örnekte, assertFalse()
başarılı olacaktır. Beklediğiniz bu değilse, Mockito'nun gerçek yöntemi şöyle çağırdığından emin olun:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
Argüman Eşleştiriciler
Önceki bölümlerde, alaycı yöntemlerimizi tam değerlerle argüman olarak yapılandırdık. Bu durumlarda, Mockito, beklenen değerlerin gerçek değerlere eşit olup olmadığını kontrol etmek için yalnızca equals()
i dahili olarak çağırır.
Ancak bazen bu değerleri önceden bilemeyiz.
Belki bir argüman olarak iletilen gerçek değeri umursamıyoruz veya belki daha geniş bir değerler aralığı için bir tepki tanımlamak istiyoruz. Tüm bu senaryolar (ve daha fazlası), argüman eşleştiricilerle ele alınabilir. Fikir basit: Kesin bir değer sağlamak yerine, Mockito'nun yöntem argümanlarıyla eşleşmesi için bir argüman eşleştirici sağlarsınız.
Aşağıdaki parçacığı göz önünde bulundurun:
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
Encode()'a hangi değeri iletirsek geçirelim sonucun aynı olduğunu görebilirsiniz, çünkü o ilk satırda anyString()
encode()
eşleştiricisini kullandık. Bu satırı düz İngilizce olarak yeniden yazarsak, “parola kodlayıcıdan herhangi bir dizeyi kodlaması istendiğinde, 'exact' dizesini döndür” gibi görünür.
Mockito, tüm bağımsız değişkenleri eşleştiricilere veya kesin değerlere göre sağlamanızı gerektirir. Bu nedenle, bir yöntemin birden fazla argümanı varsa ve argüman eşleştiricilerini yalnızca bazı argümanları için kullanmak istiyorsanız, unutun. Bunun gibi kod yazamazsınız:
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
Hatayı düzeltmek için, a
için eq
argüman eşleştiricisini içerecek şekilde son satırı aşağıdaki gibi değiştirmeliyiz:
when(mock.call(eq("a"), anyInt())).thenReturn(true);
Burada eq()
ve anyInt()
argüman eşleştiricilerini kullandık, ancak başka pek çok tane de kullanılabilir. Argüman eşleştiricilerin tam listesi için org.mockito.ArgumentMatchers
sınıfındaki belgelere bakın.
Argüman eşleştiricileri doğrulama veya saplama dışında kullanamayacağınızı unutmamak önemlidir. Örneğin, aşağıdakilere sahip olamazsınız:
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockito, yanlış yerleştirilmiş argüman eşleştiriciyi algılar ve bir InvalidUseOfMatchersException
. Argüman eşleştiricilerle doğrulama şu şekilde yapılmalıdır:
verify(mock).encode(or(eq("a"), endsWith("b")));
Argüman eşleştiriciler de dönüş değeri olarak kullanılamaz. Mockito, anyString()
veya herhangi bir şeyi döndüremez; aramaları durdururken kesin bir değer gereklidir.
Özel Eşleştiriciler
Mockito'da henüz mevcut olmayan bazı eşleştirme mantığı sağlamanız gerektiğinde özel eşleştiriciler kurtarmaya gelir. Argümanları önemsiz olmayan bir şekilde eşleştirme ihtiyacı, tasarımda bir sorun olduğunu veya bir testin çok karmaşıklaştığını gösterdiğinden, özel bir eşleştirici oluşturma kararı hafife alınmamalıdır.
Bu nedenle, özel bir eşleştirici yazmadan önce isNull()
ve nullable()
gibi bazı esnek argüman eşleştiricileri kullanarak bir testi basitleştirip basitleştiremeyeceğinizi kontrol etmeye değer. Hala bir argüman eşleştirici yazma ihtiyacı hissediyorsanız, Mockito bunu yapmak için bir dizi yöntem sunar.
Aşağıdaki örneği göz önünde bulundurun:
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
Burada hasLuck
argüman eşleştiricisini oluşturuyoruz ve eşleştiriciyi sahte bir yönteme argüman olarak geçirmek için argThat()
kullanıyoruz, dosya adı “şans” ile bitiyorsa onu true
döndürmek için saplıyoruz. ArgumentMatcher
işlevsel bir arayüz olarak ele alabilir ve örneğini bir lambda ile oluşturabilirsiniz (bu, örnekte yaptığımız şeydir). Daha az özlü sözdizimi şöyle görünür:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
İlkel türlerle çalışan bir bağımsız değişken eşleştirici oluşturmanız gerekiyorsa, bunun için org.mockito.ArgumentMatchers
birkaç yöntem daha vardır:
- charThat(ArgumentMatcher<Character> eşleştirici)
- booleanThat(ArgumentMatcher<Boolean> eşleştirici)
- byteThat(ArgumentMatcher<Byte> eşleştirici)
- shortThat(ArgumentMatcher<Short> eşleştirici)
- intThat(ArgumentMatcher<Integer> eşleştirici)
- longThat(ArgumentMatcher<Long> eşleştirici)
- floatThat(ArgumentMatcher<Float> eşleştirici)
- doubleThat(ArgumentMatcher<Double> eşleştirici)
Eşleştiricileri Birleştirme
Bir koşul, temel eşleştiricilerle işlenemeyecek kadar karmaşık olduğunda, her zaman özel bir bağımsız değişken eşleştirici oluşturmaya değmez; bazen eşleştiricileri birleştirmek işe yarayacaktır. Mockito, hem ilkel hem de ilkel olmayan türlerle eşleşen bağımsız değişken eşleştiricilerde ortak mantıksal işlemleri ('değil', 've', 'veya') uygulamak için bağımsız değişken eşleştiriciler sağlar. Bu eşleştiriciler, org.mockito.AdditionalMatchers
sınıfında statik yöntemler olarak uygulanır.
Aşağıdaki örneği göz önünde bulundurun:
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
Burada iki bağımsız değişken eşleştiricinin sonuçlarını birleştirdik: eq("1")
ve contains("a")
. Son ifade, or(eq("1"), contains("a"))
, "argüman dizesinin "1"e eşit olması veya "a" içermesi gerektiği şeklinde yorumlanabilir.
org.mockito.AdditionalMatchers
sınıfında daha az yaygın olarak listelenen, geq()
, leq()
, gt()
ve lt()
gibi, java.lang.Comparable
.
Doğrulama Davranışı
Bir sahte veya casus kullanıldığında, belirli etkileşimlerin gerçekleştiğini verify
. Kelimenin tam anlamıyla, “Hey Mockito, bu yöntemin bu argümanlarla çağrıldığından emin olun” diyoruz.

Aşağıdaki yapay örneği göz önünde bulundurun:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
Burada bir alay oluşturduk ve onun encode()
yöntemini çağırdık. Son satır, sahtenin encode()
yönteminin a
belirli bağımsız değişken değeriyle çağrıldığını doğrular. Stubbed bir çağrıyı doğrulamanın gereksiz olduğunu lütfen unutmayın; önceki snippet'in amacı, bazı etkileşimler gerçekleştikten sonra doğrulama yapma fikrini göstermektir.
Son satırı farklı bir argümana (örneğin, b
) sahip olacak şekilde değiştirirsek, önceki test başarısız olur ve Mockito, gerçek çağrının farklı argümanları olduğundan şikayet eder (beklenen a
yerine b
).
Argüman eşleştiriciler, tıpkı saplama için olduğu gibi doğrulama için kullanılabilir:
verify(passwordEncoder).encode(anyString());
Varsayılan olarak Mockito, yöntemin bir kez çağrıldığını doğrular, ancak istediğiniz sayıda çağrıyı doğrulayabilirsiniz:
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
verify()
işlevinin nadiren kullanılan bir özelliği, esas olarak eşzamanlı kodu test etmek için yararlı olan bir zaman aşımında başarısız olma yeteneğidir. Örneğin, password kodlayıcımız verify()
ile aynı anda başka bir iş parçacığında çağrılırsa, aşağıdaki gibi bir test yazabiliriz:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
encode()
çağrılır ve 500 milisaniye veya daha kısa sürede bitirilirse bu test başarılı olur. Belirttiğiniz sürenin tamamını beklemeniz gerekiyorsa, timeout()
after()
öğesini kullanın:
verify(passwordEncoder, after(500)).encode("a");
Diğer doğrulama modları ( times()
, atLeast()
, vb.), daha karmaşık testler yapmak için timeout()
ve after()
ile birleştirilebilir:
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
Times( times()
dışında, desteklenen doğrulama modları arasında only()
, atLeast()
ve atLeastOnce()
( atLeast(1)
için bir diğer ad olarak).
Mockito ayrıca bir grup sahtede arama sırasını doğrulamanıza olanak tanır. Çok sık kullanılan bir özellik değildir ancak çağrıların sırası önemliyse faydalı olabilir. Aşağıdaki örneği göz önünde bulundurun:
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
Simüle edilen çağrıların sırasını yeniden düzenlersek, test VerificationInOrderFailure
ile başarısız olur.
Çağrıların yokluğu, doğrulamaZeroInteractions verifyZeroInteractions()
kullanılarak da doğrulanabilir. Bu yöntem, bir alayı veya alayları argüman olarak kabul eder ve örnek(ler)de geçirilen herhangi bir metot çağrıldığında başarısız olur.
Ayrıca, bir argüman olarak alayları aldığından ve bu alaylar üzerindeki her çağrının doğrulandığını kontrol etmek için kullanılabildiğinden, verifyNoMoreInteractions()
yönteminden bahsetmeye değer.
Argümanları Yakalamak
Mockito, bir yöntemin belirli argümanlarla çağrıldığını doğrulamanın yanı sıra, daha sonra bunlar üzerinde özel iddialar çalıştırabilmeniz için bu argümanları yakalamanıza izin verir. Başka bir deyişle, "Hey Mockito, bu yöntemin çağrıldığını doğrulayın ve çağrıldığı argüman değerlerini bana verin" diyorsunuz.
Bir PasswordEncoder
örneği oluşturalım, encode()
çağıralım, argümanı yakalayalım ve değerini kontrol edelim:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
Gördüğünüz gibi, doğrulama için encode()
argümanı olarak passwordCaptor.capture()
'yi geçiyoruz; bu, dahili olarak bağımsız değişkeni kaydeden bir bağımsız değişken eşleştirici oluşturur. Ardından, passwordCaptor.getValue()
ile yakalanan değeri alıyoruz ve bunu assertEquals()
ile inceliyoruz.
Birden çok çağrıda bir argüman yakalamamız gerekirse, ArgumentCaptor
getAllValues()
ile tüm değerleri almanızı sağlar, şöyle:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
Aynı teknik, değişken arite yöntemi argümanlarını (varargs olarak da bilinir) yakalamak için kullanılabilir.
Basit Örneğimizi Test Etme
Artık Mockito hakkında daha çok şey bildiğimize göre, demomuza geri dönme zamanı. isValidUser
method testini yazalım. İşte böyle görünebilir:
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
API altında dalış
Mockito, okunabilir, kullanışlı bir API sağlar, ancak sınırlamalarını anlamak ve garip hatalardan kaçınmak için bazı dahili çalışmalarını inceleyelim.
Aşağıdaki snippet çalıştırıldığında Mockito'nun içinde neler olduğunu inceleyelim:
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
Açıkçası, ilk satır bir alay oluşturur. Mockito, verilen sınıfın bir alt sınıfını oluşturmak için ByteBuddy'yi kullanır. Yeni sınıf nesnesi, demo.mockito.PasswordEncoder$MockitoMock$1953422997
gibi oluşturulmuş bir ada sahiptir, equals()
kimlik denetimi görevi görür ve hashCode()
bir kimlik karma kodu döndürür. Sınıf oluşturulup yüklendikten sonra, örneği Objenesis kullanılarak oluşturulur.
Bir sonraki satıra bakalım:
when(mock.encode("a")).thenReturn("1");
Sıralama önemlidir: Burada yürütülen ilk ifade mock.encode("a")
şeklindedir ve bu, sahte üzerinde null
varsayılan dönüş değeriyle encode()
işlevini çağırır. Yani gerçekten, while when()
argümanı olarak null
değerini geçiyoruz. Mockito, ne when()
öğesine tam olarak hangi değerin iletildiğini umursamaz, çünkü alaylı bir yöntemin çağrılmasıyla ilgili bilgileri, bu yöntem çağrıldığında 'devam eden saplama' olarak depolar. Daha sonra, When when()
'i çağırdığımızda, Mockito devam eden stubbing nesnesini çeker ve onu while when()
'ın sonucu olarak döndürür. Sonra döndürülen devam eden saplama nesnesi üzerinde thenReturn(“1”)
çağırırız.
Üçüncü satır, mock.encode("a");
basit: Stubbed yöntemini çağırıyoruz. Dahili olarak Mockito, daha fazla doğrulama için bu çağrıyı kaydeder ve kütlenmiş çağrı yanıtını döndürür; bizim durumumuzda, bu dize 1
.
Dördüncü satırda ( verify(mock).encode(or(eq("a"), endsWith("b")));
), Mockito'dan bunlarla bir encode()
çağrısı olduğunu doğrulamasını istiyoruz. özel argümanlar.
Önce verify()
yürütülür, bu da Mockito'nun dahili durumunu doğrulama moduna dönüştürür. Mockito'nun durumunu ThreadLocal
içinde tuttuğunu anlamak önemlidir. Bu, güzel bir sözdizimi uygulamayı mümkün kılar, ancak diğer yandan, çerçeve yanlış kullanılırsa garip davranışlara yol açabilir (örneğin, doğrulama veya saplama dışında argüman eşleştiriciler kullanmaya çalışırsanız).
Peki Mockito bir or
eşleştiriciyi nasıl yaratır? İlk olarak, eq("a")
çağrılır ve eşleştirici yığınına bir equals
eşleştiricisi eklenir. İkinci olarak, endsWith("b")
çağrılır ve yığına bir endsWith
eşleştiricisi eklenir. Sonunda or(null, null)
çağrılır—yığından çıkardığı iki eşleştiriciyi kullanır, or
eşleştiriciyi oluşturur ve bunu yığına iter. Son olarak, encode()
çağrılır. Mockito daha sonra yöntemin beklenen sayıda ve beklenen bağımsız değişkenlerle çağrıldığını doğrular.
Argüman eşleştiriciler değişkenlere çıkarılamazken (çağrı sırasını değiştirdiği için), yöntemlere çıkarılabilirler. Bu, arama sırasını korur ve yığını doğru durumda tutar:
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
Varsayılan Yanıtları Değiştirme
Önceki bölümlerde, herhangi bir sahte yöntem çağrıldığında "boş" bir değer döndürecek şekilde mock'larımızı oluşturduk. Bu davranış yapılandırılabilir. Mockito tarafından sağlananlar uygun değilse, kendi org.mockito.stubbing.Answer
uygulamanızı bile sağlayabilirsiniz, ancak bu, birim testleri çok karmaşık hale geldiğinde bir şeylerin yanlış olduğunun bir göstergesi olabilir. KISS ilkesini hatırlayın!
Mockito'nun önceden tanımlanmış varsayılan yanıt teklifini inceleyelim:
RETURNS_DEFAULTS
varsayılan stratejidir; bir alay kurarken açıkça bahsetmeye değmez.CALLS_REAL_METHODS
, stubbed olmayan çağrıların gerçek metotları çağırmasını sağlar.RETURNS_SMART_NULLS
, stubbed olmayan bir yöntem çağrısı tarafından döndürülen bir nesneyi kullanırkennull
yerineSmartNull
döndürerekNullPointerException
önler.NullPointerException
ile yine başarısız olacaksınız, ancakSmartNull
, stubbed yönteminin çağrıldığı satırda size daha güzel yığın izlemesi sağlar. Bu,RETURNS_SMART_NULLS
varsayılan yanıt olmasını değerli kılar!RETURNS_MOCKS
önce sıradan "boş" değerleri döndürmeye çalışır, ardından mümkünse alay eder ve aksi takdirdenull
olur. Boşluk ölçütü daha önce gördüklerimizden biraz farklıdır: Dizeler ve diziler içinnull
değer döndürmek yerine,RETURNS_MOCKS
ile oluşturulan alaylar sırasıyla boş dizeler ve boş diziler döndürür.RETURNS_SELF
, inşaatçılarla alay etmek için kullanışlıdır. Bu ayarla, alay edilen sınıfın sınıfına (veya bir üst sınıfına) eşit bir türde bir şey döndüren bir yöntem çağrılırsa, bir alay kendisinin bir örneğini döndürür.RETURNS_DEEP_STUBS
,RETURNS_MOCKS
öğesinden daha derine iner ve taklitlerden vb. sahtelerden taklitler döndürebilen taklitler oluşturur.RETURNS_MOCKS
aksine, boşluk kurallarıRETURNS_DEEP_STUBS
içinde varsayılandır, bu nedenle dizeler ve diziler içinnull
değerini döndürür:
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
Sahte Adlandırma
Mockito, bir testte çok sayıda alayınız varsa ve bunları ayırt etmeniz gerekiyorsa faydalı bir özellik olan bir alayı adlandırmanıza olanak tanır. Bununla birlikte, taklitleri adlandırmaya ihtiyaç duymak, kötü tasarımın bir belirtisi olabilir. Aşağıdakileri göz önünde bulundur:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito şikayet edecek, ancak alaycıları resmi olarak adlandırmadığımız için hangisi olduğunu bilmiyoruz:
Wanted but not invoked: passwordEncoder.encode(<any string>);
Yapım aşamasında bir dize ileterek isimlendirelim:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
Artık hata mesajı daha dostça ve açıkça robustPasswordEncoder
işaret ediyor:
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
Implementing Multiple Mock Interfaces
Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Aşağıdaki örneği göz önünde bulundurun:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
Diğer ayarlar
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.