Test Yazmak: Geliştirme Sürecindeki Hayati Adım

Gökhan Ayrancıoğlu
5 min readApr 11, 2023

--

Test yazmayı anlatmadan önce neden test yazmalıyız konusunu biraz irdelemek gerekiyor. İyi kod yazan bazı insanlar unit testlerin gereksiz olduğunu düşünebiliyor. ‘Olur mu öyle şey’ demeyin çünkü böyle düşünen tanıdığım insanlar var.

Test yazmak basitçe sizin sisteminizin kalitesini artırıyor. İyi test yazılmış bir projede yaptığınız bir geliştirmenin ya da değişikliğin uygulamanızda herhangi bir şeyi bozup bozmadığını kolayca görebiliyorsunuz. Dahası testler her zaman gelişime açıktır. Diyelim ki bir senaryoyu atlamışsınız ve bu bir buga neden oldu. Bunu gördüğünüzde fixi geçerken hata aldığınız senaryolarıda testlere ekleyebilirsiniz. Böylece uygulamanız her geçen gün buglara daha dayanıklı ve bozulmalara karşı korumalı bir statüye kavuşur. Kodun kalitesinin artırılmasının yanında bakımı da kolaylaşmış olur.

Kalite ve bakım adımlarının iyileşmesinin yanı sıra kodun anlaşırlığıda artar. İyi kodlarla bezeli bir projede ne kadar aynı kod yazım stili takip edilsede bu kodların farklı insanlar tarafından yazdığını bazen unutuyoruz. Dolayısıyla farklı insanlar farklı anlaşırlık seviyesine sahip bir kod yapısı üretebiliyor. Testler ile birlikte kodun yapmak istediği işi, kullanıcının senaryosu ve metodun girdilerini ve çıktılarını testleri inceleyerek anlayabilir hale geliyoruz.

Kod kalitesi, bakımı ve kod anlaşırlığının artması demek zaman ve para tasarrufunu beraberinde getiriyor. Kodunuz bozulmalara karşı korunurken, olası hataları sadece testleri koşarak tespit edebiliyorsunuz. Böylece kodun oluşturabileceği herhangi bi hatadan korunurken olası para kayıplarını en aza indirgemiş oluyorsunuz. Kodda oluşabilecek hataları da daha erken teşhis ederek müdahale edip kalitenin yanında zaman tasarrufuda sağlıyorsunuz. Çıkan hatanın kullanıcının mağruz kalmasını engelleyip kullanıcı açısından da olumsuzlukları engellemiş oluyorsunuz. Gelişime açık bir test yapısından oluşan hatalara yeni testler yazıp bakımıda kolaylaştırmış oluyorsunuz. Tüm bunlar neden test yazmalıyız sorusunu cevaplıyor.

Peki Nasıl test yazacağız? Ne gibi test türleri var.

Test yazmak için onlarca test tekniği var ben genelde kullanılan ve heryerde rastlayabileceğiniz türlerden bahsetmek istiyorum.

  • Unit Test: Uygulamamızdaki en küçük parçaları ki genelde metotlardır test etmek için kullanılan en temel test türüdür diyebiliriz. Birim testleri, bir kodun belirli bir parçasını test etmek için kullanılır ve koda verilen girdilerin beklenen çıktıları verip vermediğini doğrular. Bir yazılım geliştiricinin hayatında olmazsa olmazlarındandır.
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class MyServiceTest {

@Test
fun `test add function`() {
val service = MyService()
val result = service.add(2, 3)
assertEquals(5, result)
}

@Test
fun `test subtract function`() {
val service = MyService()
val result = service.subtract(5, 3)
assertEquals(2, result)
}
}

class MyService {
fun add(a: Int, b: Int): Int {
return a + b
}

fun subtract(a: Int, b: Int): Int {
return a - b
}
}

Verdiğim bu örnek Kotlin dili kullanılarak JUnit 5 ve Spring Boot kullanılarak yazılmış bir test sınıfıdır. MyServiceTest sınıfı, MyService sınıfındaki add ve subtract fonksiyonlarının doğru çalıştığını test eden bir unit testpratiği örneğidir. Bu testte, her fonksiyon için gerekli input parametreleri verilerek fonksiyonlar çağrılıyor ve beklenen sonuçlar ile gerçek sonuçlar karşılaştırılıyor.

  • Integration Test: Bileşenlerin birbirleriyle uyumlu bir şekilde çalışmasını test etmek için kullanılır. Bu testler, farklı modüller arasında iletişim kurmak ve bunların nasıl tepki vereceğini test ederler.
// UserService.java

@Service
public class UserService {
@Autowired
private UserRepository userRepository;

public User createUser(User user) {
return userRepository.save(user);
}

public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}

// UserServiceIntegrationTest.java

@SpringBootTest
public class UserServiceIntegrationTest {
@Autowired
private UserService userService;

@Test
public void testCreateUser() {
User user = new User();
user.setName("John");
user.setEmail("john@example.com");
User savedUser = userService.createUser(user);
assertNotNull(savedUser.getId());
}

@Test
public void testGetUserById() {
User user = new User();
user.setName("John");
user.setEmail("john@example.com");
User savedUser = userService.createUser(user);
User foundUser = userService.getUserById(savedUser.getId());
assertEquals(user.getName(), foundUser.getName());
assertEquals(user.getEmail(), foundUser.getEmail());
}
}

Bu örnekte ise Spring Boot kullanılarak yazılmış bir MyServiceTest UserServiceIntegrationTest sınıfı var. Bu test sınıfı, UserService sınıfının doğru çalıştığını test ediyor. Bunun için, UserService sınıfındaki fonksiyonlar gerçek bir veritabanına bağlanarak test ediliyor. createUser fonksiyonu ile bir kullanıcı oluşturuluyor ve bu kullanıcının gerçekten veritabanına kaydedilip kaydedilmediği kontrol ediliyor. getUserById fonksiyonu ile de önce oluşturulan kullanıcının ID’si alınıyor ve bu ID’ye sahip kullanıcının gerçekten doğru bilgileri içerip içermediği kontrol ediliyor.

End-to-End (E2E) Test: Genellikle bir uygulamanın tüm işlevselliğini ve bileşenlerinin tamamını test etmek için kullanılır. Bu testler, gerçek kullanım senaryolarını taklit eder ve kullanıcı davranışlarını simüle ederek uygulamanın işleyişini kontrol eder.

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.web.client.exchange
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus

class ApiEndToEndTest {

private val restTemplate = TestRestTemplate()

@Test
fun `test creating a user`() {
val headers = HttpHeaders()
headers.set("Content-Type", "application/json")
val request = HttpEntity("{\\"name\\": \\"John\\", \\"email\\": \\"john@example.com\\"}", headers)
val response = restTemplate.postForEntity("/users", request, User::class.java)
assertEquals(HttpStatus.CREATED, response.statusCode)
assertEquals("John", response.body?.name)
assertEquals("john@example.com", response.body?.email)
}

@Test
fun `test retrieving a user by id`() {
val headers = HttpHeaders()
headers.set("Content-Type", "application/json")
val request = HttpEntity("{\\"name\\": \\"John\\", \\"email\\": \\"john@example.com\\"}", headers)
val createdUserResponse = restTemplate.postForEntity("/users", request, User::class.java)
val userId = createdUserResponse.body?.id ?: throw IllegalStateException("Created user ID cannot be null.")
val retrievedUserResponse = restTemplate.exchange("/users/$userId", HttpMethod.GET, null, User::class.java)
assertEquals(HttpStatus.OK, retrievedUserResponse.statusCode)
assertEquals("John", retrievedUserResponse.body?.name)
assertEquals("john@example.com", retrievedUserResponse.body?.email)
}
}

Bu örnek Kotlin dili kullanılarak Spring Boot ve JUnit 5 TestRestTemplate kullanılarak yazılmış bir API End-to-End (E2E) testi örneğidir. Test, HTTP istekleri aracılığıyla bir kullanıcı oluşturmayı ve bu kullanıcıyı oluşturduktan sonra oluşturulan kullanıcının ID’sini kullanarak kullanıcıyı almayı test eder.

Her bir test çeşidinde de biz sadece happy path dediğimiz doğru çalışma durumlarını kontrol etmemliyiz, aynı zamanda fail senaryoları yani hata durumlarını da kontrol etmemiz gerektiğini unutmayalım.

Nereye kadar test yazmalıyız?

Biliyoruz ki test yazmanın sonu yok. Bazen akıllara test coverage önemlimi sorusu geliyor. Önemi yok diyemeyiz ama çoğu şeyde olduğu gibi nicelikten çok nitelik önemlidir. Değerli software crafter’lardan Lemi Orhan Ergin’in çok güzel bi cümlesi var: “İçimizin rahat edeceği kadar test yazmalıyız.” Buradan benim çıkarımım şu; sadece test coverage’ı tuturmak/artırmak için değilde uygulamanın tüm parçalarının istenilen biçimde çalıştığından emin olup, tüm happy path’ler ile birlikte fail senaryolarında testlerini yazmış olmamız ve sonrasında şimdi uygulamanın sorunsuz çalışacağından eminim ve bir değişiklik yapıldığında o değişiklik eğer sistemi etkiliyorsa ben bunu testler üzerinden görebilirimi garanti etmektir. Yani %100 coverage’a sahip bir uygulamanız olabilir ama bu uygulamanın test coverage’ı testlerin kalitesini ya da hatalara karşı koruması olduğunu garanti etmeyecektir. Herşeye ve heryere test yazmak yerine bir metodun ya da senaryonun bütün fail senaryolarıyla birlikte test edilebiliyor olması gerekir. Testin türüne göre de senaryoları teste yedirmek çok önemlidir.

— Bana ulaşmak için: Twitter, Linkedin

— 1:1 görüşmeler için: Superpeer

https://gokhana.dev

Yazan Konusuyor Podcast

--

--

Gökhan Ayrancıoğlu

Software Engineer @Yemeksepeti • #Java • #Spring Boot • #Kotlin • #Spark • #Microservices • https://gokhana.dev