Jetpack Compose UI’ı Espresso ile Güvenle Test Etme Rehberi
Modern Android uygulama geliştirmede Jetpack Compose, deklaratif UI yaklaşımıyla geliştiricilere daha hızlı ve sezgisel arayüzler oluşturma imkanı sunuyor. Ancak, bu yenilikçi yaklaşım beraberinde yeni test stratejilerini de getiriyor. Kullanıcı arayüzlerinin beklendiği gibi çalıştığından emin olmak, uygulamanın kalitesi ve kullanıcı deneyimi için kritik öneme sahiptir. Bu makalede, Jetpack Compose UI’ınızı Espresso test çatısını kullanarak nasıl güvenle test edebileceğinizi adım adım inceleyecek, temelden ileri seviyeye kadar pratik bilgiler ve kod örnekleri sunacağız.
1. Compose Test Ortamı Kurulumu ve Temelleri
Jetpack Compose uygulamalarında UI testi yazmaya başlamadan önce, gerekli bağımlılıkları projenize eklemeniz ve test ortamınızı doğru bir şekilde yapılandırmanız gerekir. Bu bölüm, başlangıç için atmanız gereken ilk adımları ele almaktadır.
1.1. Gerekli Bağımlılıkları Ekleme
Compose UI testleri için AndroidX Test kütüphanesinin bir parçası olan androidx.compose.ui:ui-test-junit4 ve androidx.compose.ui:ui-test-manifest bağımlılıklarını projenizin build.gradle (Module: app) dosyasına eklemeniz gerekir. Ayrıca, Espresso’nun temel işlevselliği için androidx.test.espresso:espresso-core da faydalı olabilir.
dependencies {
// ... diğer bağımlılıklar
// Compose UI Test Bağımlılıkları
debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.7"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.7"
// Espresso Core (isteğe bağlı, ancak faydalı)
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
// JUnit (testler için)
androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}
debugImplementation ile eklenen ui-test-manifest, yalnızca debug build'lerinde kullanılan bir manifest dosyasıdır ve testlerin düzgün çalışması için gereklidir.
1.2. Test Ortamını Hazırlama
Android Studio'da UI testleri genellikle app/src/androidTest/java dizini altında yer alır. Bu dizin, uygulamanızın gerçek cihazlarda veya emülatörlerde çalışan enstrümantasyon testleri için ayrılmıştır. Yeni bir test sınıfı oluşturduğunuzda, bu dizin altında yer aldığından emin olun.
1.3. Temel Bir Compose Test Sınıfı Oluşturma
Her Compose UI testi, ComposeTestRule adı verilen özel bir JUnit kuralı kullanır. Bu kural, test edilecek Compose içeriğini ayarlamanıza ve UI ile etkileşime geçmenize olanak tanır.
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MyComposeUITest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun myFirstComposeTest() {
// Test edilecek Compose içeriğini ayarla
composeTestRule.setContent {
// MyComposable() gibi test edilecek bir Composable buraya gelir
Greeting("Android")
}
// UI öğesini bul ve doğrula
composeTestRule.onNodeWithText("Hello Android!")
.assertExists()
}
}
Bu örnekte, @RunWith(AndroidJUnit4::class) testlerin AndroidJUnit4 test koşucusu ile çalışmasını sağlar. @get:Rule ile tanımlanan composeTestRule, her testten önce ve sonra gerekli kurulum ve temizliği otomatik olarak yapar.
2. Compose Test Kuralı (ComposeTestRule) ve Temelleri
ComposeTestRule, Jetpack Compose UI testlerinin kalbidir. Bu kural, testlerinizin Compose UI hiyerarşisiyle etkileşime geçmesini sağlayan bir dizi yöntem sunar.
2.1. createComposeRule() Kullanımı
createComposeRule() fonksiyonu, testleriniz için bir ComposeTestRule örneği oluşturur. Bu kural, Compose UI'ın bir aktivite içinde başlatılmasını veya izole bir şekilde test edilmesini yönetir. Eğer testleriniz bir aktiviteye ihtiyaç duymuyorsa (örneğin, sadece küçük bir Composable'ı test ediyorsanız), createComposeRule() yeterlidir. Ancak, bir aktiviteye ihtiyacınız varsa createAndroidComposeRule kullanmanız gerekir.
2.2. Test Edilecek Composables'ı Tanımlama (setContent)
composeTestRule.setContent { ... } bloğu, test etmek istediğiniz Compose UI'ını tanımladığınız yerdir. Bu blok içinde, uygulamanızın herhangi bir Composable fonksiyonunu çağırabilirsiniz. Test ortamı, bu Composable'ı sanal bir ekranda render eder ve böylece onunla etkileşime geçebilir ve durumunu doğrulayabilirsiniz.
composeTestRule.setContent {
// Burada uygulamanızın temasını veya diğer sağlayıcılarını da ekleyebilirsiniz
MyApplicationTheme {
MyScreen(viewModel) // Bir ekranı veya bir bileşeni test edebilirsiniz
}
}
2.3. Senkronizasyon ve İdle Durumlar
Compose test kuralı, varsayılan olarak Compose UI'daki animasyonlar, çizimler ve diğer asenkron işlemler tamamlanana kadar bekler. Bu otomatik senkronizasyon, testlerinizin UI'ın kararlı bir durumda olduğunu varsaymasını sağlar. Ancak, bazen özel durumlar veya uzun süreli asenkron işlemler için composeTestRule.waitForIdle() gibi manuel senkronizasyon yöntemleri kullanmanız gerekebilir.
3. Espresso ile Compose UI Öğelerini Bulma (Matchers)
Espresso'da UI öğelerini bulmak için "Matcher"lar kullanılır. Compose testlerinde bu match'ler, Compose'un semantik ağacı üzerinde çalışır ve öğeleri metinlerine, içerik açıklamalarına veya test etiketlerine göre bulmanızı sağlar.
3.1. onNodeWithText, onNodeWithContentDescription, onNodeWithTag
* onNodeWithText(text: String): Belirli bir metin içeren bir Compose düğümünü bulur. Örneğin, bir Text Composable'ı veya bir Button üzerindeki metni bulmak için kullanılır.
* onNodeWithContentDescription(description: String): Erişilebilirlik için ayarlanmış içerik açıklamasına sahip bir düğümü bulur. Genellikle görsel öğeler (ikonlar, resimler) için kullanılır.
* onNodeWithTag(tag: String): Geliştiricinin Modifier.testTag("myTag") ile özel olarak etiketlediği bir düğümü bulur. Bu, testler için öğeleri benzersiz bir şekilde tanımlamanın en güvenilir yollarından biridir.
// Metin ile bulma
composeTestRule.onNodeWithText("Kaydet").performClick()
// İçerik açıklaması ile bulma
composeTestRule.onNodeWithContentDescription("Ayarlar ikonu").assertIsDisplayed()
// Test etiketi ile bulma
composeTestRule.onNodeWithTag("username_input").performTextInput("kullaniciadi")
3.2. Semantik Ağaç ve Erişilebilirlik
Compose testleri, UI hiyerarşisinin bir soyutlaması olan "semantik ağaç" üzerinde çalışır. Bu ağaç, kullanıcıların ve erişilebilirlik servislerinin etkileşime girdiği bilgileri içerir. Testlerinizde onNodeWithContentDescription gibi yöntemleri kullanmak, uygulamanızın erişilebilirlik standartlarına uygunluğunu da dolaylı olarak test etmenizi sağlar. Modifier.semantics { ... } ile semantik özellikleri manuel olarak ayarlayabilirsiniz.
3.3. Birden Fazla Eşleşen Öğeyi Yönetme
Bazen birden fazla UI öğesi aynı metne veya özelliğe sahip olabilir. Bu durumda, onNodeWithText("Gönder", ignoreCase = true) gibi yöntemler birden fazla eşleşme hatası verebilir. Bu tür senaryolarda, onAllNodesWithText("Gönder")[0] ile belirli bir indeksi hedefleyebilir veya onNodeWithText("Gönder").assertCountEquals(1) ile tek bir eşleşme beklediğinizi belirtebilirsiniz. En iyi pratik, onNodeWithTag kullanarak benzersiz etiketler sağlamaktır.
4. Etkileşimler ve Eylemler (Actions)
UI öğelerini bulduktan sonra, kullanıcıların onlarla nasıl etkileşime geçtiğini simüle etmeniz gerekir. Compose test kütüphanesi, bu etkileşimleri gerçekleştirmek için çeşitli perform eylemleri sunar.
4.1. Tıklama (performClick())
Bir düğmeye, listeye veya başka bir tıklanabilir öğeye tıklama eylemini simüle eder.
composeTestRule.onNodeWithText("Giriş Yap").performClick()
4.2. Metin Girişi (performTextInput())
Bir TextField veya BasicTextField bileşenine metin girmek için kullanılır.
composeTestRule.onNodeWithTag("email_input")
.performTextInput("test@example.com")
4.3. Kaydırma (performScrollTo())
Bir kaydırılabilir kapsayıcı içindeki bir öğenin görünür olana kadar kaydırılmasını sağlar. Özellikle uzun listeler veya kaydırılabilir ekranlar için kullanışlıdır.
composeTestRule.onNodeWithText("Uzun Listenin Son Öğesi")
.performScrollTo()
.assertIsDisplayed()
4.4. Özel Eylemler
Compose test kütüphanesi, performGesture veya performTouchInput gibi daha düşük seviyeli API'ler aracılığıyla kaydırma, sürükleme, sıkıştırma gibi daha karmaşık hareketleri de simüle etmenize olanak tanır. Bu, özel UI bileşenlerinin veya jestlerin test edilmesi için faydalıdır.
// Örnek: Özel bir kaydırma hareketi
composeTestRule.onNodeWithTag("scrollable_area")
.performTouchInput {
swipeUp() // Yukarı kaydırma
}
5. Doğrulamalar (Assertions)
Bir UI öğesiyle etkileşime geçtikten sonra, uygulamanızın UI'ının beklenen duruma ulaştığını doğrulamak için assert yöntemlerini kullanırsınız.
5.1. Metin Doğrulaması (assertTextEquals(), assertTextContains())
Bir öğenin belirli bir metni içerip içermediğini veya tam olarak o metne eşit olup olmadığını kontrol eder.
composeTestRule.onNodeWithTag("welcome_message")
.assertTextEquals("Hoş Geldiniz!")
composeTestRule.onNodeWithText("Ürün Listesi")
.assertTextContains("Listesi")
5.2. Görünürlük Doğrulaması (assertIsDisplayed(), assertIsNotDisplayed())
Bir öğenin ekranda görünüp görünmediğini kontrol eder. Bu, genellikle bir öğenin koşullu olarak render edildiği durumlarda kullanılır.
composeTestRule.onNodeWithText("Hata Mesajı")
.assertIsNotDisplayed() // Başlangıçta görünmemeli
// Bir eylem sonrası görünür hale gelirse
composeTestRule.onNodeWithText("Hata Mesajı")
.assertIsDisplayed()
5.3. Durum Doğrulaması (assertIsEnabled(), assertIsSelected(), assertIsFocused())
Bir öğenin etkin (enabled), seçili (selected) veya odaklanmış (focused) gibi belirli bir duruma sahip olup olmadığını doğrular.
composeTestRule.onNodeWithText("Gönder Butonu")
.assertIsNotEnabled() // Form geçerli değilse devre dışı olmalı
// Form doldurulduktan sonra
composeTestRule.onNodeWithText("Gönder Butonu")
.assertIsEnabled()
5.4. Semantik Özellik Doğrulaması
assert() ile daha genel semantik özellikler üzerinde doğrulama yapabilirsiniz. Örneğin, bir öğenin tıklanabilir olup olmadığını veya belirli bir rolü olup olmadığını kontrol edebilirsiniz.
composeTestRule.onNodeWithText("Ayarlar")
.assert(hasClickAction()) // Tıklanabilir olduğunu doğrula
6. Gelişmiş Test Senaryoları ve İpuçları
Daha karmaşık uygulamalar için, Compose UI testlerinizi daha etkili ve sürdürülebilir hale getirecek bazı ileri düzey teknikler ve ipuçları mevcuttur.
6.1. Kapsamlı Testler İçin Modüler Yaklaşım
Büyük ekranları veya karmaşık bileşenleri test ederken, her şeyi tek bir test fonksiyonunda yapmak yerine, testleri daha küçük, yönetilebilir parçalara ayırmak faydalıdır. Her Composable veya ekran için ayrı bir test sınıfı oluşturmak, testlerin okunabilirliğini ve bakımını artırır. Ayrıca, test helper fonksiyonları veya Page Object Model benzeri desenler kullanarak test kodunuzu daha modüler hale getirebilirsiniz.
6.2. Test Helper Fonksiyonları
Tekrar eden test adımlarını veya karmaşık etkileşimleri soyutlamak için helper fonksiyonları yazabilirsiniz. Örneğin, bir giriş formunu doldurmak için bir fonksiyon:
fun fillLoginForm(email: String, password: String) {
composeTestRule.onNodeWithTag("email_input").performTextInput(email)
composeTestRule.onNodeWithTag("password_input").performTextInput(password)
composeTestRule.onNodeWithText("Giriş Yap").performClick()
}
@Test
fun testSuccessfulLogin() {
composeTestRule.setContent { LoginForm() }
fillLoginForm("user@example.com", "password123")
composeTestRule.onNodeWithText("Hoş Geldiniz!").assertIsDisplayed()
}
6.3. Animasyonları ve Zamanlamayı Yönetme
Compose test kuralı, varsayılan olarak animasyonların bitmesini bekler. Ancak, bazen animasyonları devre dışı bırakmak veya belirli bir animasyonun tamamlanmasını beklemek isteyebilirsiniz. composeTestRule.mainClock.autoAdvance = false ile otomatik zaman ilerlemesini kapatıp composeTestRule.mainClock.advanceTimeBy(durationMillis) ile manuel olarak ilerletebilirsiniz.
6.4. Çoklu Ekran Boyutları İçin Test Stratejileri
Farklı ekran boyutları ve yönelimleri için UI'ınızın doğru şekilde render edildiğini test etmek önemlidir. Bunun için farklı cihaz konfigürasyonlarını simüle eden testler yazabilir veya belirli bir Composable'ın farklı boyutlarda nasıl davrandığını kontrol etmek için composeTestRule.setContent içinde BoxWithConstraints gibi bileşenler kullanabilirsiniz.
7. Test Edilebilir Compose Kod Yazma Pratikleri
Testlerinizi daha kolay ve etkili hale getirmek için Compose kodunuzu belirli prensiplere göre yazmanız önemlidir.
7.1. Durum Yönetimini Ayırma (Stateless Composables)
Composables'larınızı mümkün olduğunca "stateless" (durumsuz) olarak tasarlayın. Yani, durumları Composable'ın dışından parametre olarak alsınlar ve olayları geri çağırma (callback) fonksiyonları aracılığıyla dışarıya bildirsinler. Bu, Composable'ları izole bir şekilde test etmeyi kolaylaştırır.
// Test edilmesi kolay stateless Composable
@Composable
fun MyButton(text: String, onClick: () -> Unit, isEnabled: Boolean) {
Button(onClick = onClick, enabled = isEnabled) {
Text(text)
}
}
7.2. Test Etiketleri (testTag) Kullanımı
Modifier.testTag("my_unique_tag") kullanarak UI öğelerinize benzersiz etiketler atayın. Bu etiketler, öğeleri testlerde güvenilir bir şekilde bulmanın en iyi yoludur, çünkü metin veya içerik açıklamaları değişebilirken test etiketleri genellikle sabit kalır.
TextField(
value = username,
onValueChange = { username = it },
modifier = Modifier.testTag("username_input")
)
7.3. Bağımlılık Enjeksiyonu ve Mocking
ViewModel'lar veya repository'ler gibi bağımlılıkları olan Composables'ları test ederken, gerçek bağımlılıklar yerine sahte (mock) bağımlılıklar kullanın. Hilt veya Koin gibi bağımlılık enjeksiyon kütüphaneleri, testlerde sahte bağımlılıkları sağlamayı kolaylaştırır. Bu, UI testlerinizin hızlı ve izole kalmasını sağlar, dış servislerin veya veri kaynaklarının durumundan etkilenmez.
Sonuç
Jetpack Compose, UI geliştirmede devrim niteliğinde bir değişim sunarken, sağlam test stratejileriyle desteklenmesi gereken bir teknolojidir. Espresso ve Compose test kütüphanesini kullanarak, Compose UI'ınızı güvenle test edebilir, kullanıcı deneyiminin tutarlı ve hatasız olmasını sağlayabilirsiniz. ComposeTestRule ile test ortamını kurmaktan, onNodeWithTag ile öğeleri bulmaya, performClick ile etkileşime geçmeye ve assertIsDisplayed ile doğrulamaya kadar öğrendiğiniz bu yöntemler, Compose uygulamanızın kalitesini artırmak için temel taşlardır. Test edilebilir kod yazma pratiklerini benimseyerek ve ileri düzey test senaryolarını uygulayarak, geliştirme sürecinizi daha verimli ve güvenilir hale getirebilirsiniz.
SSS (Sık Sorulan Sorular)
1. Compose UI testleri ile geleneksel Android UI testleri arasındaki temel fark nedir?
Geleneksel Android UI testleri (Espresso ile View tabanlı), UI hiyerarşisini ve View objelerini doğrudan manipüle eder. Compose UI testleri ise Compose'un deklaratif yapısını kullanarak "semantik ağaç" üzerinde çalışır. Bu, testlerin daha soyut ve bileşen tabanlı olmasını sağlar, UI'ın iç çalışma detaylarından ziyade kullanıcı deneyimine odaklanır.
2. Compose UI testlerinde Mockito gibi kütüphaneleri kullanabilir miyim?
Evet, kesinlikle. Mockito veya MockK gibi mocking kütüphaneleri, ViewModel'lar, repository'ler veya diğer bağımlılıkları sahte nesnelerle değiştirmek için Compose UI testlerinde yaygın olarak kullanılır. Bu, UI testlerinizin sadece UI katmanına odaklanmasını ve dış bağımlılıkların davranışlarından etkilenmemesini sağlar.
3. Asenkron işlemleri (örneğin, API çağrıları) Compose UI testlerinde nasıl yönetirim?
Compose test kuralı, varsayılan olarak Compose'daki asenkron işlemlerin (örneğin, LaunchedEffect, rememberCoroutineScope içindeki coroutine'ler) tamamlanmasını bekler. Ancak, harici API çağrıları gibi daha uzun süreli işlemler için, ViewModel'larınızda veya repository'lerinizde sahte (mock) veri döndürerek bu işlemleri simüle etmelisiniz. Bu, testlerinizin hızlı ve deterministik kalmasını sağlar.
4. Compose UI testlerinde sık karşılaşılan tuzaklar nelerdir?
* Yanlış eşleştiriciler (matchers): Birden fazla öğeyi eşleştiren veya hiç eşleşmeyen match'ler kullanmak. testTag kullanımı bu sorunu büyük ölçüde çözer.
* Senkronizasyon sorunları: Özellikle karmaşık animasyonlar veya manuel zaman ilerlemesi kullanılan durumlarda UI'ın kararlı bir duruma gelmesini beklememek.
* Bağımlılıkların sahtelenmemesi: Gerçek bağımlılıklarla test yapmak, testlerin yavaşlamasına ve dış etkenlere bağımlı hale gelmesine neden olur.
* Erişilebilirlik eksikliği: contentDescription veya semantics modifier'larını kullanmamak, hem erişilebilirlik sorunlarına yol açar hem de testlerde öğeleri bulmayı zorlaştırır.