projeler genelde büyük bir enerji ile başlar. hele bir de çevik programlama ve test güdümlü programlama söylemlerinin büyüsüne kapılmış bir ekibiniz varsa, harıl harıl birim test yazılmaya başlanır.
zaman problemi olmayan bu enerjik ekip hiç bir şeyden üşenmez. sahte sunucular havalarda uçuşur, veritabanında çılgın sahte veriler oluşturulur. testi yazılmamış tek bir metod bulamazsınız.
sonra projelerin ortalarına doğru, yazılan kodları birleştirme aşamasına gelindiğinde, odak geliştirmek değil birleştirmek olduğundan yeni test eklenmemeye başlanır. varolan testler ise sınırlı ölçüde güncellenmeye devam eder. metodun kapsamı genişlemişse, genişleyen kapsam için test eklenmez, sadece daha önce yazılmış testlerin geçmesinin sağlayacak şekilde en az değişiklik felsefesiyle test güncellemeleri yapılır.
projenin sonuna doğru ise ekibimizde ne enerji ne de zaman kalmıştır. ama kod değişmeye devam eder. bu zamanlarda projede "önce çalışsın" havası hakim olur. yazılmış olan testler patlamaya başladıkça yorum haline getirilir.
ve gün gelir, bakmışsınız, elinizde hiç kayda değer bir birim test kalmamış, projenin başında harcadığınız onca çabayı çöpe atmışsınız.
bu duruma düşmemek için birim testler konusunda ekibin bilinçlendirilmesi şart. herkesin şu gerçekleri kabul etmesi gerek:
- yaz ve unut birim testi yoktur. test kodunu yazarken bir gün bu kodun başkası tarafından güncellenmesi gerekeceğini unutma.
- birim test yazımı kodun kendisinin yazımından daha uzun sürer, daha çok emek ister.
- birim test ile fonksiyonel test ayrı şeylerdir. birim test ile test etmeye çalıştığımız kendi yazdığımız kod, kullandığımız veritabanının veya bağlandığımız web servisinin kodu değil!
- test ettiğin metod alt metodlar çağırıyorsa, alt metodun yerine getirdiği işlevin doğrulamasını burada yapma! sadece alt metodun doğru paremetrelerle çağrılıp çağrılmadığını ve alt metodun döndüğü değerlere testini yazdığımız fonksiyon doğru tepki veriyor mu kontrolleri için test yaz.
- testleri yorum haline getirmek testleri geçirme yöntemi değildir.
- test kodundan harici bir sunucuya, veritabanına vs. bağlanmaya çalışma.
- mümkünse birim testler ön ayar gerektirmesin. projenin kodlarını çekip "ant clean test" demek birim testleri çalıştırmak için yeterli olsun.
- uygulamayı üzerinde geliştirdiğin "framework"'u birim testler için ayağa kaldırma.
- projenin tüm birim testlerini çalıştırmak 1 dk.'yi geçmesin.
sahte nesne yaratmak için, özellikle javada bir çok gelişmiş kütüphane mevcut. yukarıda söylediğim şartları yerine getirebilmeniz için kullandığınız kütüphanenin şu özellikleri deskteklemesi gerek:
- 'constructor' üzerine yazabilmelisiniz (metod içerisinde "new" ile yaratılmış nesneleri sahte nesnelerle değiştirebilmek için).
- final/static/native/private metod ve değişkenlerinin üzerine yazabilmelisiniz.
- test ettiğiniz sınıfın bir kısmını sahte hale getirebiliyor olmalısınız.
ben burada jmockit üzerinden bazı pratik örnekler vermeye çalışacağım:
- sınıfın bir kısmının üzerine yazmak.
- statik metodun üzerine yazmak.
- private değişkene değer atamak.
- metod içerisinde new ile yarattığın bir nesnenin üzerine yazmak.
public interface IFoo {
public String bar();
}
public class Foo implements IFoo {birim testi yazılacak örnek sınıf:
public Foo() {
}
public String bar() {
return "foobar";
}
}
public class Sample {örnek birim test:
private String privateField = "sample";
private IFoo fooNotInitiated = null;
private IFoo fooInitiatedAtConstructor = null;
private IFoo fooInitiatedAtDefinition = new Foo();
public Sample() {
this.fooInitiatedAtConstructor = new Foo();
}
public Sample(IFoo fooPassed) {
this.fooInitiatedAtConstructor = fooPassed;
}
public static String staticMethodA() {
return "a";
}
public static String staticMethodBCallingA() {
return staticMethodA() + ",b";
}
public String instanceMethodUsingPrivateFieldAndNewFoo() {
Foo foo = new Foo();
return privateField + ":" + foo.bar();
}
public String instanceMethodUsingPrivateFooNotInitiated() {
return fooNotInitiated.bar();
}
public String instanceMethodUsingPrivateFooInitiatedAtDefinition() {
return fooInitiatedAtDefinition.bar();
}
public String instanceMethodUsingPrivateFooInitiatedAtConstructor() {
return fooInitiatedAtConstructor.bar();
}
}
import junit.framework.TestCase;
import mockit.Mock;
import mockit.Mockit;
import mockit.MockUp;
import static mockit.Deencapsulation.setField;
public class SampleTest extends TestCase {
private Sample instance;
protected void setUp() {
instance = new Sample();
}
public void testStaticMethodA() {
assertEquals("a", Sample.staticMethodA());
}
public void testStaticMethodBCallingA() {
Mockit.setUpMock(Sample.class, new Object() {
@Mock
public String staticMethodA() {
return "m";
}
});
assertEquals("m,b", Sample.staticMethodBCallingA());
}
public void testOverridingNewOperator() {
new MockUp() {
@Mock
void $init() {
}
@Mock
public String bar() {
return "moobar";
}
};
assertEquals("sample:moobar", instance.instanceMethodUsingPrivateFieldAndNewFoo());
setField(instance, "privateField", "mample");
assertEquals("mample:moobar", instance.instanceMethodUsingPrivateFieldAndNewFoo());
assertEquals("moobar", instance.instanceMethodUsingPrivateFooInitiatedAtConstructor());
assertEquals("moobar", instance.instanceMethodUsingPrivateFooInitiatedAtDefinition());
instance = new Sample(new Foo());
assertEquals("sample:moobar", instance.instanceMethodUsingPrivateFieldAndNewFoo());
assertEquals("moobar", instance.instanceMethodUsingPrivateFooInitiatedAtConstructor());
assertEquals("moobar", instance.instanceMethodUsingPrivateFooInitiatedAtDefinition());
}
public void testCreatingMockFromInterface() {
IFoo mockFoo = new MockUp() {
@Mock
public String bar() {
return "moobar";
}
}.getMockInstance();
try {
assertEquals("moobar", instance.instanceMethodUsingPrivateFooNotInitiated());
fail("expecting null pointer exception here!");
} catch(java.lang.NullPointerException e) {
}
setField(instance, "fooNotInitiated", mockFoo);
assertEquals("moobar", instance.instanceMethodUsingPrivateFooNotInitiated());
assertEquals("foobar", instance.instanceMethodUsingPrivateFooInitiatedAtDefinition());
assertEquals("foobar", instance.instanceMethodUsingPrivateFooInitiatedAtConstructor());
instance = new Sample(new Foo());
assertEquals("foobar", instance.instanceMethodUsingPrivateFooInitiatedAtConstructor());
assertEquals("foobar", instance.instanceMethodUsingPrivateFooInitiatedAtDefinition());
setField(instance, "fooNotInitiated", mockFoo);
setField(instance, "fooInitiatedAtConstructor", mockFoo);
setField(instance, "fooInitiatedAtDefinition", mockFoo);
assertEquals("moobar", instance.instanceMethodUsingPrivateFooNotInitiated());
assertEquals("moobar", instance.instanceMethodUsingPrivateFooInitiatedAtDefinition());
assertEquals("moobar", instance.instanceMethodUsingPrivateFooInitiatedAtConstructor());
}
}
derleyip birim testleri çalıştıran kod:
#!/bin/sh
javac -cp .:jmockit/jmockit.jar:junit-4.9b2.jar Sample.java SampleTest.java && java -javaagent:jmockit/jmockit.jar -cp jmockit/jmockit.jar:junit-4.9b2.jar:. org.junit.runner.JUnitCore SampleTest