Oluşturma düzeninizi test etme

Kullanıcı arayüzlerini veya ekranları test etmek, Composer kodunuzun doğru davranışını doğrulamak için kullanılır. Böylece, hataları geliştirme sürecinin erken aşamalarında yakalayarak uygulamanızın kalitesini iyileştirir.

Compose, öğeleri bulmak, özelliklerini doğrulamak ve kullanıcı işlemlerini gerçekleştirmek için bir dizi test API'si sağlar. Ayrıca, zaman manipülasyonu gibi gelişmiş özellikler de içerir.

Anlambilim

Oluşturma'daki kullanıcı arayüzü testleri, kullanıcı arayüzü hiyerarşisiyle etkileşimde bulunmak için anlam bilimi kullanır. Adından da anlaşılacağı gibi, anlamlar bir kullanıcı arayüzünü anlamlandırır. Bu bağlamda, bir "kullanıcı arayüzü parçası" (veya öğe), tek bir composable'dan tam ekrana kadar her şey anlamına gelebilir. Anlamsal ağaç, kullanıcı arayüzü hiyerarşisiyle birlikte oluşturulur ve açıklar.

Tipik bir kullanıcı arayüzü düzenini ve bu düzenin karşılık gelen semantik ağacıyla nasıl eşleneceğini gösteren şema

Şekil 1. Tipik bir kullanıcı arayüzü hiyerarşisi ve anlambilim ağacı.

Anlamsal çerçeve öncelikle erişilebilirlik için kullanılır. Bu nedenle testler, kullanıcı arayüzü hiyerarşisiyle ilgili anlamların açığa çıkardığı bilgilerden yararlanır. Nelerin ne kadarının gösterileceğine geliştiriciler karar verir.

Grafik ve metin içeren bir düğme

2. Şekil. Bir simge ve metin içeren tipik bir düğme.

Örneğin, simge ve metin öğesinden oluşan bunun gibi bir düğme verildiğinde, varsayılan anlam ağacı yalnızca "Beğen" metin etiketini içerir. Bunun nedeni, Text gibi bazı composable'ların bazı özellikleri semantik ağacına zaten göstermesidir. Anlamsal ağacına Modifier kullanarak özellik ekleyebilirsiniz.

MyButton(
    modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)

Kurulum

Bu bölümde, oluşturma kodunu test etmenizi sağlamak için modülünüzü nasıl ayarlayacağınız açıklanmaktadır.

Öncelikle, kullanıcı arayüzü testlerinizi içeren modülün build.gradle dosyasına aşağıdaki bağımlılıkları ekleyin:

// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Bu modülde bir ComposeTestRule ve Android için AndroidComposeTestRule adlı bir uygulama bulunur. Bu kuralı kullanarak "İçerik oluştur"u ayarlayabilir veya etkinliğe erişebilirsiniz. Oluşturma için tipik kullanıcı arayüzü testi şuna benzer:

// file: app/src/androidTest/java/com/package/MyComposeTest.kt

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    // use createAndroidComposeRule<YourActivity>() if you need access to
    // an activity

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = fakeUiState, /*...*/)
            }
        }

        composeTestRule.onNodeWithText("Continue").performClick()

        composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
    }
}

API'leri test etme

Öğelerle etkileşimde bulunmanın üç temel yolu vardır:

  • Bulucular, iddialarda bulunmak veya üzerinde işlem yapmak için bir veya daha fazla öğe (ya da anlam ağacındaki düğümler) seçmenize olanak tanır.
  • Onaylar, öğelerin mevcut olduğunu veya belirli özelliklere sahip olduğunu doğrulamak için kullanılır.
  • İşlemler, öğelere tıklama veya diğer hareketler gibi simüle edilen kullanıcı etkinliklerini ekler.

Bu API'lerin bazıları, anlam ağacında bir veya daha fazla düğüme referans vermek için bir SemanticsMatcher kabul eder.

Bulucular

onNode ve onAllNodes seçeneklerini sırasıyla bir veya daha fazla düğüm seçmek için kullanabilirsiniz, ancak onNodeWithText, onNodeWithContentDescription gibi en yaygın aramalar için kolaylık bulucuları da kullanabilirsiniz. Tüm listeye Test Oluşturma ile ilgili yardımcı kısa bilgiler'de göz atabilirsiniz.

Tek bir düğüm seçin

composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
    .onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")

Birden fazla düğüm seçin

composeTestRule
    .onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
    .onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")

Ayrılmış ağacı kullanma

Bazı düğümler, alt öğelerinin anlamsal bilgilerini birleştirir. Örneğin, iki metin öğesi içeren bir düğme bunların etiketlerini birleştirir:

MyButton {
    Text("Hello")
    Text("World")
}

Bir testte, anlam ağacını göstermek için printToLog() öğesini kullanabiliriz:

composeTestRule.onRoot().printToLog("TAG")

Bu kod aşağıdaki çıkışı yazdırır:

Node #1 at (...)px
 |-Node #2 at (...)px
   Role = 'Button'
   Text = '[Hello, World]'
   Actions = [OnClick, GetTextLayoutResult]
   MergeDescendants = 'true'

Ayrılmış ağacın düğümünü eşleştirmeniz gerekirse useUnmergedTree değerini true olarak ayarlayabilirsiniz:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")

Bu kod aşağıdaki çıkışı yazdırır:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = '[Hello]'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = '[World]'

useUnmergedTree parametresi, tüm bulucularda kullanılabilir. Örneğin, burada onNodeWithText bulucuda kullanılmıştır.

composeTestRule
    .onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()

Onaylama

Bir veya daha fazla eşleştiriciye sahip bulucu tarafından döndürülen SemanticsNodeInteraction üzerinde assert() yöntemini çağırarak onayları kontrol edin:

// Single matcher:
composeTestRule
    .onNode(matcher)
    .assert(hasText("Button")) // hasText is a SemanticsMatcher

// Multiple matchers can use and / or
composeTestRule
    .onNode(matcher).assert(hasText("Button") or hasText("Button2"))

assertExists, assertIsDisplayed, assertTextEquals gibi en yaygın onaylamalar için kolaylık işlevlerini de kullanabilirsiniz. Bu araçların tam listesine, Test Oluşturma ile ilgili yardımcı kısa bilgiler'den göz atabilirsiniz.

Bir düğüm koleksiyonundaki onayları kontrol etmek için de işlevler vardır:

// Check number of matched nodes
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())

İşlemler

Bir düğüme işlem eklemek için bir perform…() işlevini çağırın:

composeTestRule.onNode(...).performClick()

İşlemlere ilişkin bazı örnekleri aşağıda bulabilirsiniz:

performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }

Tam listeye göz atmak için Test Oluşturma ile ilgili yardımcı kısa bilgilere göz atabilirsiniz.

Eşleştiriciler

Bu bölümde, Yazma kodunuzu test etmek için kullanabileceğiniz bazı eşleyiciler açıklanmaktadır.

Hiyerarşik eşleştiriciler

Hiyerarşik eşleştiriciler, anlam ağacında aşağı veya yukarı gidip basit eşleme yapmanıza olanak tanır.

fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher):  SemanticsMatcher

Aşağıda, bu eşleştiricilerin kullanımına ilişkin bazı örnekler verilmiştir:

composeTestRule.onNode(hasParent(hasText("Button")))
    .assertIsDisplayed()

Seçiciler

Test oluşturmanın alternatif bir yolu, bazı testleri daha okunabilir hale getirebilecek seçiciler kullanmaktır.

composeTestRule.onNode(hasTestTag("Players"))
    .onChildren()
    .filter(hasClickAction())
    .assertCountEquals(4)
    .onFirst()
    .assert(hasText("John"))

Tam listeye göz atmak için Test Oluşturma ile ilgili yardımcı kısa bilgilere göz atabilirsiniz.

Senkronizasyon

Oluşturma testleri varsayılan olarak kullanıcı arayüzünüzle senkronize edilir. ComposeTestRule üzerinden bir onay veya işlem çağırdığınızda test, kullanıcı arayüzü ağacı boşta kalana kadar beklenerek önceden senkronize edilir.

Normalde herhangi bir işlem yapmanız gerekmez. Ancak bilmeniz gereken bazı uç durumlar vardır.

Bir test senkronize edildiğinde Compose uygulamanız sanal bir saat kullanarak gelişmiş olur. Bu, Compose testlerinin gerçek zamanlı olarak çalışmayacağı ve dolayısıyla olabildiğince hızlı bir şekilde geçebileceği anlamına gelir.

Ancak testlerinizi senkronize eden yöntemleri kullanmazsanız yeniden oluşturma yapılmaz ve kullanıcı arayüzü duraklatılmış olarak görünür.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions
    var lastSeenValue = 0 // Used to track recompositions
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition

    // Fails because nothing triggered a recomposition
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition
    composeTestRule.onNodeWithText("1").assertExists()
}

Ayrıca bu gereksinimin uygulamanın geri kalanı için değil, yalnızca Oluşturma hiyerarşileri için geçerli olduğunu unutmayın.

Otomatik senkronizasyonu devre dışı bırakma

ComposeTestRule üzerinden bir onaylama veya işlem (ör. assertExists()) çağırdığınızda testiniz Oluştur kullanıcı arayüzüyle senkronize edilir. Bazı durumlarda bu senkronizasyonu durdurup saati kendiniz kontrol etmek isteyebilirsiniz. Örneğin, kullanıcı arayüzünün meşgul olacağı bir noktada animasyonun doğru ekran görüntülerini alma süresini kontrol edebilirsiniz. Otomatik senkronizasyonu devre dışı bırakmak için mainClock içindeki autoAdvance özelliğini false olarak ayarlayın:

composeTestRule.mainClock.autoAdvance = false

Genellikle saati siz ilerlersiniz. advanceTimeByFrame() ile tam olarak bir kare veya advanceTimeBy() ile belirli bir süre ilerleyebilirsiniz:

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Boşta kalan kaynaklar

Compose, testleri ve kullanıcı arayüzünü senkronize edebilir. Böylece her işlem ve onaylama işlemi boşta durumdayken tamamlanır. Bununla birlikte, sonuçları kullanıcı arayüzü durumunu etkileyen bazı eşzamansız işlemler, test bunların farkında değilken arka planda çalıştırılabilir.

Test edilen uygulamanın meşgul ya da boşta olup olmadığına karar verirken bu kaynakların dikkate alınması için, testinize bu boşta kalan kaynakları oluşturup kaydedebilirsiniz. Boşta olan ek kaynakları kaydetmeniz gerekmediği sürece (örneğin, Espresso veya Compose ile senkronize edilmeyen bir arka plan işi çalıştırıyorsanız) herhangi bir işlem yapmanız gerekmez.

Bu API, test edilen konunun boşta veya meşgul olduğunu gösteren Espresso'nun Boşta Kalma Kaynakları özelliğine çok benzer. IdlingResource uygulamasının kullanımını kaydetmek için Test oluşturma kuralını kullanırsınız.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

Manuel senkronizasyon

Bazı durumlarda, Compose kullanıcı arayüzünü testinizin veya test ettiğiniz uygulamanın diğer bölümleriyle senkronize etmeniz gerekir.

waitForIdle, Compose'un boşta kalmasını bekler ancak autoAdvance özelliğine bağlıdır:

composeTestRule.mainClock.autoAdvance = true // default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle

Her iki durumda da waitForIdle öğesinin bekleyen çekme ve düzen geçişlerini bekleyeceğini unutmayın.

Ayrıca, advanceTimeUntil() ile belirli bir koşul karşılanana kadar saati ilerletebilirsiniz.

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

Belirtilen koşulun, bu saatten etkilenebilecek durumu kontrol etmesi gerektiğini unutmayın (yalnızca Oluştur durumunda çalışır).

Koşullar bekleniyor

Veri yükleme veya Android'in ölçüsü ya da çizimi (yani Compose'un dışında ölçüm veya çizim yapma) gibi harici çalışmaya bağlı tüm koşullarda, waitUntil() gibi daha genel bir kavram kullanılmalıdır:

composeTestRule.waitUntil(timeoutMs) { condition }

waitUntil yardımcılarından herhangi birini de kullanabilirsiniz:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Yaygın kalıplar

Bu bölümde, Test oluşturma bölümünde göreceğiniz bazı genel yaklaşımlar açıklanmaktadır.

Tek başına test etme

ComposeTestRule, tüm composable'ları (uygulamanızın tamamı, tek bir ekran veya küçük bir öğe) görüntüleyen bir etkinlik başlatmanıza olanak tanır. composable'ların doğru şekilde kapsüllendiğinden ve bağımsız olarak çalıştıklarından emin olmak, daha kolay ve daha odaklanmış kullanıcı arayüzü testlerine olanak sağlamak için de iyi bir uygulamadır.

Bu, yalnızca birim kullanıcı arayüzü testlerini oluşturmanız gerektiği anlamına gelmez. Kullanıcı arayüzünün büyük kısmının kapsamını belirlemek de çok önemlidir.

Kendi içeriğinizi oluşturduktan sonra etkinliğe ve kaynaklara erişin

Çoğu zaman test edilmekte olan içeriği composeTestRule.setContent kullanarak ayarlamanız ve ayrıca etkinlik kaynaklarına erişmeniz gerekir. Örneğin, görüntülenen bir metnin bir dize kaynağıyla eşleştiğini doğrulamak için işlem yapmanız gerekir. Ancak etkinlik önceden çağrılıyorsa createAndroidComposeRule() ile oluşturulmuş bir kuralda setContent numaralı telefonu çağıramazsınız.

Bunu başarmak için sıklıkla kullanılan bir kalıp, boş etkinlik (ComponentActivity gibi) kullanarak bir AndroidComposeTestRule oluşturmaktır.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = exampleUiState, /*...*/)
            }
        }
        val continueLabel = composeTestRule.activity.getString(R.string.next)
        composeTestRule.onNodeWithText(continueLabel).performClick()
    }
}

ComponentActivity uygulamasının, uygulamanızın AndroidManifest.xml dosyasına eklenmesi gerektiğini unutmayın. Bunu, modülünüze şu bağımlılığı ekleyerek yapabilirsiniz:

debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Özel anlambilim özellikleri

Bilgileri testlere göstermek için özel anlambilim özellikleri oluşturabilirsiniz. Bunun için yeni bir SemanticsPropertyKey tanımlayın ve SemanticsPropertyReceiver kullanarak bunu kullanılabilir hale getirin.

// Creates a Semantics property of type Long
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey

Artık semantics değiştiricisini kullanarak bu mülkü kullanabilirsiniz:

val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
    modifier = Modifier.semantics { pickedDate = datePickerValue }
)

Testlerde, özelliğin değerini doğrulamak için SemanticsMatcher.expectValue kullanabilirsiniz:

composeTestRule
    .onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
    .assertExists()

Eyalet geri yüklemesini doğrulama

Etkinlik veya işlem yeniden oluşturulduğunda Compose öğelerinizin durumunun doğru şekilde geri yüklendiğini doğrulamanız gerekir. Bu tür bir kontrol, StateRestorationTester sınıfıyla etkinlik yeniden oluşturmaya gerek kalmadan yapılabilir.

Bu sınıf, bir composable'ın yeniden oluşturulmasını simüle etmenizi sağlar. Bu, özellikle rememberSaveable'in uygulandığını doğrulamak için yararlıdır.


class MyStateRestorationTests {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun onRecreation_stateIsRestored() {
        val restorationTester = StateRestorationTester(composeTestRule)

        restorationTester.setContent { MainScreen() }

        // TODO: Run actions that modify the state

        // Trigger a recreation
        restorationTester.emulateSavedInstanceStateRestore()

        // TODO: Verify that state has been correctly restored.
    }
}

Hata ayıklama

Testlerinizde sorunları çözmenin ana yolu, anlambilim ağacına bakmaktır. Testinizin herhangi bir noktasında composeTestRule.onRoot().printToLog() yöntemini çağırarak ağacı yazdırabilirsiniz. Bu işlev, aşağıdakine benzer bir günlük yazdırır:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = 'Hi'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = 'There'

Bu günlükler, hataları takip etmek için değerli bilgiler içerir.

Espresso ile birlikte çalışabilirlik

Karma uygulamalarda, Compose bileşenlerini görünüm hiyerarşilerinin içinde ve görünümlerinde Compose composable'lar içinde (AndroidView composable aracılığıyla) bulabilirsiniz.

Her iki türle de eşleştirmek için gereken özel bir adım yoktur. Görünümleri Espresso'nun onView ile, Compose öğelerini ise ComposeTestRule ile eşleştirirsiniz.

@Test
fun androidViewInteropTest() {
    // Check the initial state of a TextView that depends on a Compose state:
    Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
    // Click on the Compose button that changes the state
    composeTestRule.onNodeWithText("Click here").performClick()
    // Check the new value
    Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}

UiAutomator ile birlikte çalışabilirlik

Varsayılan olarak, composable'lara UiAutomator'dan yalnızca kullanışlı tanımlayıcıları (gösterilen metin, içerik açıklaması vb.) göre erişilebilir. Modifier.testTag kullanan herhangi bir composable'a erişmek istiyorsanız ilgili composables alt ağacı için testTagAsResourceId semantik özelliğini etkinleştirmeniz gerekir. Bu davranışı etkinleştirmek, kaydırılabilir composable'lar gibi başka benzersiz herkese açık kullanıcı adı olmayan composable'lar (ör. LazyColumn) için kullanışlıdır.

Modifier.testTag ile iç içe yerleştirilmiş tüm composable'lara UiAutomator'dan erişilebildiğinden emin olmak için bunu composable'lar hiyerarşinizde yalnızca bir kez üst üste etkinleştirebilirsiniz.

Scaffold(
    // Enables for all composables in the hierarchy.
    modifier = Modifier.semantics {
        testTagsAsResourceId = true
    }
){
    // Modifier.testTag is accessible from UiAutomator for composables nested here.
    LazyColumn(
        modifier = Modifier.testTag("myLazyColumn")
    ){
        // content
    }
}

Modifier.testTag(tag) ile tüm composable'lar, resourceName ile aynı tag kullanılarak By.res(resourceName) kullanılarak erişilebilir.

val device = UiDevice.getInstance(getInstrumentation())

val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn

Daha fazla bilgi

Daha fazla bilgi edinmek için Jetpack Compose Test codelab'ini deneyin.

Numuneler