5 Adımda Daha Kaliteli Yazılım Testleri

yazar:

kategori:

Yazılım testleri can yeleği olma özelliğine sahip olmalarına rağmen, yazılımcılar tarafından göz ardı edilme eğilimi yaşayan en önemli yazılım disiplinlerinden birisidir. Çalıştığım projelerde testlerin hak ettikleri ilgi ve alakayı görmediklerini gözlemliyorum. Edindiğim izlenimler şu şekilde:

  • Bazı yazılımcılar test yazma konusuna hiç ilgi göstermezler.
  • Bazı yazılımcılar test yazarlar, lakin test kodunu işletme mantığının yer aldığı koddan ayrı tutalar, yani test koduna üvey evlat muamelesi yaparlar. Bu test kodunun zaman içinde çok karmaşık hale gelmesi ya da zamanla gerçekleri yansıtmaması anlamına gelebilir.
  • Bazı yazılımcılar test yazarlar. Test koduna üvey evlat muamelesi yapmamalarına rağmen, test kodunun okunabilirliğini artırmak için efor sarf etmezler. Bu tür testlerin anlaşılması, bakımı ve geliştirilmeleri zordur.
  • Bazı yazılımcılar test kodunun ne kadar kıymetli olduğunu bilirler ve güncel ve sade kalmaları için ellerinden gelen her türlü çabayı sarf ederler.

Testleri uygulamayı kullanan sanal kullanıcılar olarak düşünebiliriz. Testlere baktığımız zaman, işletme mantığının yer aldığı kodu incelemeden, uygulamanın nasıl çalıştığı hakkında çok kısa bir sürede bilgi sahibi olabiliriz. Bunun gerçekleşebilmesi için testlerin sade bir dilde yazılmış olmaları gerekmektedir. Bu yazımda bu amaca nasıl yaklaşabileceğimizi göstermek istiyorum.

Aşağıda Customer sınıfını test ettiğini zanneden, ama aslında ödeme işlemini test eden bir birim testi yer almaktadır.

[source language=”java”]
// Kod 1

@Test
public void testCustomer() throws Exception {

final Customer customer = new Customer();
customer.setName("Acar");
customer.setFirstname("Oezcan");

final Order order = new Order();
order.setOrderAmount(100);
order.setCustomer(customer);

final Payment payment = new Payment();

final PaymentResult result = payment.pay(order);
Assert.assertTrue(result.success);

}
[/source]

Test metot isimleri metot bünyesinde yapılan işlemleri ifade edecek güçte olmalıdırlar. Metot ismine bakarak, neyin test edildiğini anlamak kolaydır ya da kolay olmalıdır. Bu yüzden seçilen ismin gerçekleri yansıtması gerekir. Bu yüzden metot ismini ilk etapta testPayment olarak değiştirmemizde fayda görüyorum. Daha sonra bu ismi değiştirerek, yapılan spesifik testin ne olduğunu ifade etmesini sağlayacağız.

Kod 1 de yer alan test kodunun ilk bakışta karmaşık bir yapıda olduğunu görüyoruz. Bunun başlıca sebebi müşteri, sipariş ve ödeme nesnelerinin oluşturulmaları ve metot bünyesinde gerekli veriler ile donatılmalarıdır. Bu gereğinden fazla kodun oluşmasına sebep olmaktadır. Bir metot bünyesindeki kod sayısı arttıkça, kodun okunurluk seviyesi düşer. Bu yüzden bir metoda mümkün mertebe az kodun konuşlandırılmasını sağlamamız gerekmektedir.

Kod 1 de yer alan test metodunun okunurluk seviyesini nasıl artırabiliriz? Test metodunun merkezinde Order sınıfı yer almaktadır. Order sınıfı için bir fluent (akıcı arayüz) interface oluşturarak, nesne oluşturma ve işlem yapma sürecini şu şekilde daha okunur hale getirebiliriz.

[source language=”java”]
// Kod 2

Customer customer;

@Before
public void setup() {
customer = new Customer("Oezcan", "Acar");
}

@Test
public void testPayment() throws Exception {

final Order order = OrderBuilder.order().withCustomer(customer).withOrderAmount(100)
.returnOrderObject();

final Payment payment = new Payment();

final PaymentResult result = payment.pay(order);
Assert.assertTrue(result.success);
}

public static class OrderBuilder {

Customer customer;
Order order;

public PaymentResult pay(final Order order) {
return new PaymentResult(true);
}

public Order returnOrderObject() {
if (order == null) {
throw new IllegalStateException("lütfen daha önce order nesnesini olusturunuz!");
}
return this.order;
}

public OrderBuilder withOrderAmount(final int amount) {
order = new Order();
order.setOrderAmount(amount);
if (customer != null) {
order.setCustomer(customer);
}
return this;

}

public OrderBuilder withCustomer(final Customer customer) {
this.customer = customer;
return this;

}

public static OrderBuilder order() {
return new OrderBuilder();

}
}
[/source]

OrderBuilder sınıfını fluent interface türünde oluşturdum. Kod 1 e baktığımızda, müşteri ve sipariş oluşturma işleminin altı satırı kapsadığını görmekteyiz. Kod 2 de fluent interface yardımı ile tek bir satırda sipariş nesnesini oluşturduk.

Aynı şeyi Payment sınıfı için de şu şekilde yapabiliriz:

[source language=”java”]
// Kod 3

Customer customer;

@Before
public void setup() {
customer = new Customer("Oezcan", "Acar");
}

@Test
public void testPayment() throws Exception {

final Order order = OrderBuilder.order().withCustomer(customer).withOrderAmount(100)
.returnOrderObject();

final PaymentResult paymentResult = PaymentBuilder.payment().withOrder(order).pay();

Assert.assertTrue(paymentResult.success);
}

public static class PaymentBuilder {

Order order;

public static PaymentBuilder payment() {
return new PaymentBuilder();
}

public PaymentResult pay() {
final Payment payment = new Payment();
return payment.pay(this.order);

}

public PaymentBuilder withOrder(final Order order) {
this.order = order;
return this;
}

}
[/source]

Her test metodu bünyesinde test etme süreci şu adımlardan oluşur ya da oluşmalıdır:

  • Birinci adımda test edilen nesneler oluşturulur. Bu işlemi test için gerekli ortamın oluşturulması olarak düşünebiliriz.
  • İkinci adımda test edilmek istenen metot koşturulur.
  • Üçüncü adımda elde edilen netice sahip olunan beklentilerle kıyaslanır.

Davranış güdümlü yazılımda (BDD – Behavior Driven Development) metotlar ya da birimler değil, uygulamanın sahip oldugu davranışlar test edilir. Bu tür testler uygulamayı bir kara kutu olarak görürler. Bu şekilde detaylarda kaybolmadan, uygulamanın sahip olması gereken davranış biçimlerini test etmek mümkündür. BDD dünyasında bir davranışı test etmek için Given/When/Then yapısı kullanılır. Given testin çıkış noktasıdır. Bunu test edilen davranış için gerekli ortamın hazırlanması olarak düşünebiliriz. When bölümünde uygulamanın davranışı tetiklenir. Then bölümünde uygulamanın nasıl bir davranış sergilediği test edilir. Bizim uygulamamızda Given/When/Then yapısını şu şekilde uygulayabiliriz:

[source language=”java”]
// Kod 4

@Test
public void testPayment() throws Exception {

// Given
final Order order = OrderBuilder.order().withCustomer(customer).withOrderAmount(100)
.returnOrderObject();

// When
final PaymentResult paymentResult = PaymentBuilder.payment().withOrder(order).pay();

// Then
Assert.assertTrue(paymentResult.success);
}
[/source]

Kod 4 bünyesinde BDD tarzı aralar kullanmasak da, testimizi ve beklentilerimiz BDD tarzı yapılandırdık. Bu şekilde hangi işlemlerin yapıldığını bir bakışta anlamak daha kolay bir hale gelmektedir. Buna rağmen OrderBuilder ve PaymentBuilder sınıfları çok fazla detayı ihtiva ettiklerinden, ilk bakışta ne yaptıklarını anlamak zor olabilir. O halde kod 4 de yer alan test metodunu daha sadeleştirmemiz gerekmektedir. Bunun bir örneği şu şekilde olabilirdi:

[source language=”java”]
// Kod 5

@Test
public void testPayment() throws Exception {

// Given
final Order order = createOrderWithAnAmountOf(100);

// When
final PaymentResult paymentResult = payNow(order);

// Then
assertPaymentIsSuccessfull(paymentResult);
}

private Order createOrderWithAnAmountOf(final double value) {
final Order order = OrderBuilder.order().withCustomer(customer).withOrderAmount(value)
.returnOrderObject();
return order;
}

private PaymentResult payNow(final Order order) {
final PaymentResult paymentResult = PaymentBuilder.payment().withOrder(order).pay();
return paymentResult;
}

private void assertPaymentIsSuccessfull(final PaymentResult paymentResult) {
Assert.assertTrue(paymentResult.success);
}
[/source]

Then bölümünde testten olan beklentilerimiz yer almaktadır. Bu yazımda birim testlerinden olan beklentilerimizi daha net nasıl ifade edebileceğimiz konusuna değinmiştim. Yine burada fluent interface yardımı ile beklentilerimizi daha okunaklı hale getirebiliriz.

Kod 5 de yer alan testi herhangi bir BDD aracı kullanmadan oluşturduk. Sadece BDD prensiplerini uygulamaya çalıştık. BDD yapmak için kullanabileceğiniz test araçları bulunmaktadır. Bunlardan birisi easyb. Easyb Groovy dilini kullandığından dolayı, testlerimizin ifade gücünü artırmak için gerçek anlamda DSL (Domain Specific Languages) yapıları oluşturabiliyor ve kullanabiliyoruz. Kod 4 de yer alan testi easyb ile şu şekilde tanımlayabiliriz:

[source language=”java”]
Kod 6

scenario "Müsterinin 100 TL’lik yaptigi alisverisin ödeme bölümünü test ediyoruz.", {

given "Müsteri 100 TL’lik alisveris yapar",{

order = OrderBuilder.order().withCustomer(customer).withOrderAmount(100)
.returnOrderObject();

}

when "Müsteri kasaya gidip, ödeme butonuna tikladiginda", {

paymentResult = PaymentBuilder.payment().withOrder(order).pay();

}

then "Ödeme basariyla tamamlanir", {
Assert.assertTrue(paymentResult.success);
}

}
[/source]

Test ettiğimiz uygulama davranışı ödeme işlemidir. Easyb yardımı ile kod 6 da yer alan kodu bir test senaryosu haline getirdik. Kod 5 de yer alan kodu da bir test senaryosu olarak düşünebiliriz. Test senaryoları oluşturmak kodun daha okunur ve neyin test edildiğinin daha anlaşılır olmasını sağlamaktadırlar.

Kod 6 da kullandığımız

[source language=”java”]
scenario
given
when
then
[/source]

bir alana has dildir (DSL – Domain Specific Language). Burada içinde olduğumuz alan test alanıdır. Easyb aracılığı ile kullandığımız bilgisayar dili 4 direktif ihtiva etmekle birlikte, testlerimiz bünyesinde ifade etmek istediklerimiz için yeterli yapıdadırlar. Küçük olan bu alan dilleri ile yapılan işlemleri daha net ifade etmek mümkündür.

Son olarak test metodunun ismine tekrar bir göz atalım. testPayment çok genel bir isim gibi görünmekte. Test bünyesinde olup, bitenleri ifade etmek için ifade gücü yüksek bir metot ismi seçmemiz faydalı olacaktır. Ben aşağıda yer alan ismi öneriyorum.

[source language=”java”]
@Test
public void when_an_order_is_triggered_then_payment_should_be_successfull() throws Exception {

// Given
final Order order = createOrderWithAnAmountOf(100);

// When
final PaymentResult paymentResult = payNow(order);

// Then
assertPaymentIsSuccessfull(paymentResult);
}
[/source]

Metot isimlerini oluştururken given/when/then yapılarını kullanabiliriz. Bu metot bünyesinde yapılan işlemi birebir dışa yansıtmak için en verimli neticeyi verecektir.

Özetleyecek olursak:

  • Test metotları given/when/then şeklinde üç bölüme ayrılarak, testin okunurluğu artırılabilir.
  • Fluent interface yapıları hem given bölümünde yer alan test nesneleri yapılandırmak hem de then bölümündeki beklentilerin ifadesi için kullanılabilirler. Fluent interface kullanımı test okunurluk oranını artırır, çünkü insanların kullandığı dile yakın yapıdadırlar.
  • Metot isimleri metot bünyesinde yapılan işlemleri ifade edecek güce sahip olmalıdırlar.
  • Testlerin uygulama senaryoları olarak hazırlanmaları, neyin test edildiğinin netleşmesine ışık tutarlar.

Bu tarz test yazmayı bir deneyin ve tecrübelerinizi bizimle paylaşın.


EOF (End Of Fun)
Özcan Acar