İçeriğe geç
academia.sh

Ders 04 / 19

Arayüz Testleri

Arayüz testi bir ekranda düğümü bulur, onunla etkileşir ve sonucu doğrular. Ders düğümü metniyle aramanın anlam ağacını baştan dolaştığını, test etiketiyle aramanın tek adımda bittiğini ölçer.

Yayın Güncelleme

Bu derste

  • Anlam ağacını ve test etiketini arayüz testinin düğüm bulma araçları olarak ayırır
  • Düğümü metniyle aramanın adım sayısını ağaçtaki konuma göre hesaplar ve bütçeyle karşılaştırır
  • Test etiketiyle eşlem üzerinden aramanın her örnekte tek adım olduğunu ölçer
  • Ekran büyüyünce metinle aramanın aşımının değiştiğini, etiketle aramanın değişmediğini gösterir
İçindekiler

Önceki derste her test ekranı sıfırdan başlatıyordu ve ölçüm kurulumun maliyetini saydı. Ekran ayağa kalktıktan sonra test onunla konuşmak zorundadır: bir düğümü bulur, ona dokunur, sonucu okur. Bu konuşmanın da bir maliyeti vardır ve bu dersin konusu odur. Kursun sorusu aynı: bu ölçüm bütçesini kaç örnekte aşıyor? Dört yanıt değişmez: içinde, azında, çoğunda, hepsinde. Ölçülen iş birimi, bir düğümü bulmak için atılan adımdır.

Çekirdek önceki derslerle aynıdır: bir bütçe, bir ölçümün etiketi ve örnek listesi, sınırı kesin olarak aşan örneklerin sayısı.

// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}

Ekranın Test İçin Görünüşü

Bir arayüz testi ekrandaki pikselleri değil, ekranın anlamını okur. Bileşen tabanlı arayüzde ekranın bu anlamlı hâline anlam ağacı (semantics tree) denir. Ağaçtaki her düğüm bir metin, bir rol ya da bir eylem taşır: başlık, düğme, bir ürünün adı. Erişilebilirlik hizmetleri de aynı ağacı temel alır; düğümleri birleştirme kurallarını ise kendileri uygular. Testin işi bu ağaçta bir düğümü bulmak, ona bir eylem uygulamak ve düğümün sonraki durumunu doğrulamaktır.

Düğümü bulmanın iki yolu var. Birincisi metniyle aramaktır: kullanıcının gördüğü yazıyla düğümü bulursun. Bu doğal bir yoldur, çünkü test kullanıcının baktığı şeye bakar. İkincisi test etiketiyle aramaktır: geliştirici düğüme bir etiket (test tag) verir ve test o etiketi sorar. Etiket kullanıcıya görünmez; yalnız teste hizmet eder. Platformda ikisi şöyle görünür.

// Platform kodu: CartComposeTest
class CartComposeTest {
    @get:Rule
    val composeRule = createComposeRule()

    @Test
    fun deleteButtonIsShown() {
        composeRule.setContent { CartScreen() }
        composeRule.onNodeWithTag("sil-defter").assertIsDisplayed()
        composeRule.onNodeWithText("Defteri sil").assertIsDisplayed()
    }
}

setContent ekranı kurar, onNodeWithTag etiketle, onNodeWithText metinle düğümü bulur. Klasik görünüm sisteminde aynı iş başka bir araçla yapılır.

// Platform kodu: CartViewTest
class CartViewTest {
    @Test
    fun deleteButtonIsShownInViews() {
        onView(withId(R.id.deleteNotebook)).check(matches(isDisplayed()))
    }
}

onView görünüm ağacında kimlikle ya da metinle bir görünüm bulur. İki parça da platform koduna aittir. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde ağaç bir düğüm listesidir ve bir arama bu listede atılan adımlarla sayılır.

TA1. Model, arayüz testi kitaplığını koşturmaz; bu ortamda ekran ve ağaç yoktur. Anlam ağacı düz bir listeyle temsil edilir ve arama listeyi baştan dolaşır. Gerçek araç ağacı derinlik sırasıyla dolaşır; model bu sıralamayı listenin sırasına indirger.

Düğümü Metniyle Aramak

Bir alışveriş sepeti ekranının ağacında on iki düğüm var: başlık, geri düğmesi, iki ürün ve her ürün için adet ve silme düğmesi, toplam, kupon kodu, kuponu uygulama düğmesi ve ödemeye geçme düğmesi. Her düğümün bir metni ve benzersiz bir etiketi vardır. Yardımcı dosya ağacı kurar, metinle aramayı ve etiketle aramayı tanımlar.

// Semantics.kt — sepet ekraninin anlam agaci, metinle ve etiketle arama
class Node(val text: String, val tag: String)

fun cartTree(extra: Int = 0): List<Node> {
    val head = listOf(
        Node("Sepetim", "baslik"),
        Node("Geri", "geri"),
        Node("Defter", "urun-defter"),
        Node("Defter adedi", "adet-defter"),
        Node("Defteri sil", "sil-defter"),
        Node("Kalem", "urun-kalem"),
        Node("Kalem adedi", "adet-kalem"),
        Node("Kalemi sil", "sil-kalem"),
    )
    val offers = (1..extra).map { Node("Oneri $it", "oneri-$it") }
    val tail = listOf(
        Node("Toplam", "toplam"),
        Node("Kupon kodu", "kupon"),
        Node("Kuponu uygula", "kupon-uygula"),
        Node("Odemeye gec", "odeme"),
    )
    return head + offers + tail
}

fun pickByTags(tree: List<Node>, tags: List<String>): List<Node> =
    tags.map { tag -> tree.first { it.tag == tag } }

fun findSteps(tree: List<Node>, text: String): Int {
    var steps = 0
    for (node in tree) {
        steps++
        if (node.text == text) return steps
    }
    return steps
}

fun tagSteps(byTag: Map<String, Node>, tag: String): Int =
    if (byTag.containsKey(tag)) 1 else 0

findSteps ağacı baştan dolaşır ve metin eşleşince durur. Atılan adım, düğümün ağaçtaki sırasıdır: başlık 1 adımda, ödemeye geçme düğmesi 12 adımda bulunur. cartTree ağaca isteğe bağlı olarak öneri düğümleri ekler; şimdilik eklemiyoruz. Sekiz test sekiz düğümü arayacak: geri, defteri silme, kalem, kalem adedi, toplam, kuponu uygulama, ödemeye geçme ve başlık. Bütçe 5 adımdır: bu, bir düğümün ekranın başından beş düğüm içinde bulunmasını isteyen bir ekip kararının modeldeki karşılığıdır, bir platform eşiği değildir.

// Main.kt
fun main() {
    val tree = cartTree()
    val picks = pickByTags(
        tree,
        listOf("geri", "sil-defter", "urun-kalem", "adet-kalem", "toplam", "kupon-uygula", "odeme", "baslik"),
    )
    val steps = picks.map { findSteps(tree, it.text) }
    println("adimlar: $steps")

    val overrun = Overrun("metinle-arama", Budget("adim", 5), steps)
    overrun.line()
    countOverruns(listOf(overrun))
}
adimlar: [2, 5, 6, 7, 9, 11, 12, 1]
metinle-arama: butce=adim<=5 | asan=5/8 -> cogunda
icinde: 0
azinda: 0
cogunda: 1
hepsinde: 0

Sekiz aramanın beşi bütçeyi aştı: kalem, kalem adedi, toplam, kuponu uygulama ve ödemeye geçme düğmeleri ekranın ilk beş düğümünün ötesinde duruyor. Ölçüm çoğundadır. Başlık, geri düğmesi ve defteri silme düğmesi ağacın başında olduğu için bütçenin içinde kaldı. Aşım düğümün metninden değil, konumundan gelir: aynı metni taşıyan bir düğüm ağacın başında olsaydı arama kısa sürerdi.

Kural bu ölçümde de iki şeyi görmez. Büyüklüğü görmez: ödemeye geçme düğmesi bütçeyi yedi adımla, kalem bir adımla aşar ve ikisi de bir sayılır. Nedeni görmez: aşımın nedeni ağacın dolaşım düzenidir ve düğümlerin kullanıcıya göre dizilişidir.

Anlam Ağacının Düğümleri

Anlam ağacındaki her düğüm, ekrandaki bir öğenin teste ve erişilebilirlik hizmetlerine görünen özetidir. Bir düğüm metin taşıyabilir, bir rol bildirebilir, bir eylem sunabilir ve bir test etiketi alabilir. Ekranda iç içe duran birkaç öğe, örneğin bir kartın içindeki başlık ve açıklama, bileşen tabanlı arayüzde varsayılan olarak tek bir düğümde birleşebilir. Test bu birleşik düğümü arar; birleşmeden önceki ağaç ayrıca istenebilir. Bu ayrıntı aramanın kaç adım süreceğini değiştirir, çünkü birleşik ağaç daha az düğüm taşır.

Modelimizde on iki düğüm var ve her biri ayrı bir öğedir. Gerçek bir ekranda düğüm sayısı ekranın yapısına göre çok daha az ya da çok daha fazla olabilir. Ölçümün öğrettiği şey sayı değil, ilişkidir: arama düğümün sırasına bağlıysa, ekran büyüdükçe aşım artar.

Platform Parçalarının Satır Satır Okunuşu

CartComposeTest parçasında @get:Rule ek açıklaması, bileşen tabanlı arayüzün test kuralını her testten önce kurar ve sonra kapatır. createComposeRule() bu kuralı üretir. composeRule.setContent { CartScreen() } satırı test edilecek ekranı kurala yükler; ekran boş bir activity içinde kurulur, önceki dersteki gibi uygulamanın kendi ekranını başlatmak gerekmez. onNodeWithTag("sil-defter") etiketi sil-defter olan düğümü bulur. assertIsDisplayed() düğümün ekranda göründüğünü doğrular. onNodeWithText("Defteri sil") ise aynı düğümü metniyle bulur.

CartViewTest parçasında onView(withId(R.id.deleteNotebook)) klasik görünüm sisteminde kimliği verilen görünümü bulur. check(matches(isDisplayed())) görünümün görünür olduğunu doğrular. Kimlik, metnin yerini tutan bir etikettir: kaynak dosyada bir kez tanımlanır ve çeviriden bağımsızdır.

Bir etkileşim eklemek istersek aynı düğüm arama zincirine performClick() gibi bir eylem eklenir. Zincirin ilk halkası her zaman düğüm aramasıdır; eylem ve doğrulama bulunan düğüm üzerinde çalışır. Bu yüzden aramanın maliyeti, testteki her etkileşimin maliyetine eklenir.

Metinle Aramanın Kırılganlığı

Metinle arama yalnız yavaş değildir; metin değişince kırılır. Düğmenin yazısı “Defteri sil”den “Sil”e dönerse, metinle arayan her test kırılır. Uygulama birden çok dilde yayımlanıyorsa, test yalnız tek bir dilde geçer ve diğer dillerde düğümü bulamaz. Etiket bu iki sorunu da yaşamaz, çünkü etiket kullanıcının dilinden bağımsızdır.

Öte yandan metin, kullanıcının gerçekten gördüğü şeydir. Metinle arayan test, düğmenin kullanıcıya doğru yazıyla göründüğünü örtük olarak sınar. Etiketle arayan test bunu sınamaz ve bu fark bir seçimdir: hızlı aramayı mı, yoksa görünen yazının doğruluğunu mu istediğini ekip belirler.

Etiketlerin Adlandırılması

Etiketin değeri, kararlı kalmasında yatar. Model etiketlerin biçimini eylem-öğe olarak seçti: sil-defter, adet-kalem, kupon-uygula. Bu ad ekranın düzenine değil, öğenin işlevine bağlıdır. Düğme ekranın başka bir yerine taşınsa da etiket aynı kalır. Düzeni anlatan bir ad, örneğin ikinci-satir-sag-dugme, ekran değişince yanıltıcı olur.

Etiketin benzersiz olması da gerekir. Listedeki her satır aynı etiketi taşırsa, tek düğüm bekleyen arama birden çok eşleşme bulur ve test hata verir. Bu yüzden listelerde etikete satırı ayırt eden bir değer, örneğin ürün kodu, eklenir.

Erişilebilirlikle İlişki

Anlam ağacı yalnız testlerin değil, ekran okuyucuların da kullandığı bir yapıdır. Bu yüzden metinle arayan bir test, ekran okuyucunun okuyacağı metne yakın bir metni arar; metni doğru olmayan bir düğümü bulamayan test, erişilebilirlik sorununu da dolaylı olarak yakalar. Etiket ise yalnız testlere hizmet eder ve ekran okuyucuya görünmeyebilir. İki aramayı birlikte kullanmanın gerekçesi budur: etiket hızlı ve kararlı bulur, metin kullanıcının duyduğu şeyi doğrular.

Yaygın Yanılgılar

İlk yanılgı, “her düğüme etiket koymak testi sağlamlaştırır” düşüncesidir. Etiketler üretim koduna test amaçlı bir yük ekler ve unutulan bir etiket testi kırar. Etiket, yalnız metnin güvenilir olmadığı düğümlere konur: yinelenen metinler, dinamik metinler ve simgeler.

İkinci yanılgı, “metinle arama her zaman yavaştır” düşüncesidir. Ölçümde yavaşlık düğümün sırasından geldi. Aranan düğüm ağacın başındaysa metinle arama da kısa sürer: başlık bir adımda, geri düğmesi iki adımda bulundu. Aşım konuma bağlıdır, aramanın biçimine değil.

Üçüncü yanılgı, “ekran büyüyünce testi yeniden yazmak gerekir” düşüncesidir. Etiketle arayan testlerde büyüme aramanın maliyetini değiştirmedi. Büyüyen ekranda yeniden yazılması gereken şey, etiketsiz kalan düğümlerin aramasıdır.

Sınırlayıcı Ölçüm

Bu ölçümün sınırlayıcısı etiketle aramadır. Gerekçesi şudur: ilk ölçümde aşım, aramanın ağaçtaki konuma bağlı olmasından geliyordu. Düğümü eşlemden bulan bir arama konumu ortadan kaldırır. Düğümler, bütçe ve örnek sayısı aynı kalırsa, sonuçtaki farkı yalnız arama biçimi açıklar.

Aynı ekranı bu kez test etiketiyle arayalım. Etiket benzersizdir, bu yüzden bir eşlemde tek bakışla bulunur. Eşlemin kendisi bir kez kurulmalıdır: ağaç bir kez dolaşılır ve her düğüm etiketiyle bir eşleme konur. Bu bir kez ödenen bir iştir ve ekran başına bir defa yapılır.

// Main.kt — ayni ekran, etiketle arama
fun main() {
    val tree = cartTree()
    val picks = pickByTags(
        tree,
        listOf("geri", "sil-defter", "urun-kalem", "adet-kalem", "toplam", "kupon-uygula", "odeme", "baslik"),
    )
    val byTag = tree.associateBy { it.tag }
    println("esleme kurulumu: ${tree.size} adim, ekran basina bir kez")

    val steps = picks.map { tagSteps(byTag, it.tag) }
    println("adimlar: $steps")

    val overrun = Overrun("etiketle-arama", Budget("adim", 5), steps)
    overrun.line()
    countOverruns(listOf(overrun))
}
esleme kurulumu: 12 adim, ekran basina bir kez
adimlar: [1, 1, 1, 1, 1, 1, 1, 1]
etiketle-arama: butce=adim<=5 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0

Aynı ekran, aynı sekiz düğüm, aynı bütçe. Her arama tek adımda bitti ve ölçüm çoğundadan içinde yönüne geçti. Değişen test edilen ekran değil, düğüme ulaşma yoludur.

Etiketle arama bir şeyi de feda eder: etiket kullanıcıya görünmez. Bir düğmenin üzerindeki yazı yanlışsa metinle arayan test hatayı yakalar, etiketle arayan test yakalamaz. Platformda iki yol birlikte kullanılır: etiketle bulunur, metin ayrıca doğrulanır.

Bir üçüncü ölçüm, ekran büyüyünce ne olduğunu gösterir. Toplamın önüne sekiz öneri düğümü ekleyelim. Bütçeyi bu kez eski ağacın uzunluğuna, yani 12 adıma koyalım: metinle arama eski ekranda her düğümü bu sınırın içinde bulur.

// Main.kt — ekran buyuyunce metinle arama pahalanir, etiketle arama ayni kalir
fun main() {
    val tags = listOf("geri", "sil-defter", "urun-kalem", "adet-kalem", "toplam", "kupon-uygula", "odeme", "baslik")
    val small = cartTree()
    val large = cartTree(8)
    val smallPicks = pickByTags(small, tags)
    val largePicks = pickByTags(large, tags)
    val byTag = large.associateBy { it.tag }

    val smallText = smallPicks.map { findSteps(small, it.text) }
    val largeText = largePicks.map { findSteps(large, it.text) }
    val largeTag = largePicks.map { tagSteps(byTag, it.tag) }
    println("eski ekran, metin: $smallText")
    println("buyuk ekran, metin: $largeText")
    println("buyuk ekran, etiket: $largeTag")

    val limit = Budget("adim", 12)
    val overruns = listOf(
        Overrun("metinle-eski-ekran", limit, smallText),
        Overrun("metinle-buyuk-ekran", limit, largeText),
        Overrun("etiketle-buyuk-ekran", limit, largeTag),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
eski ekran, metin: [2, 5, 6, 7, 9, 11, 12, 1]
buyuk ekran, metin: [2, 5, 6, 7, 17, 19, 20, 1]
buyuk ekran, etiket: [1, 1, 1, 1, 1, 1, 1, 1]
metinle-eski-ekran: butce=adim<=12 | asan=0/8 -> icinde
metinle-buyuk-ekran: butce=adim<=12 | asan=3/8 -> azinda
etiketle-buyuk-ekran: butce=adim<=12 | asan=0/8 -> icinde
icinde: 2
azinda: 1
cogunda: 0
hepsinde: 0

Eski ekranda metinle arama 12 adımlık bütçenin içindeydi. Ekran sekiz düğüm büyüyünce toplam, kuponu uygulama ve ödemeye geçme düğmeleri sınırın ötesine geçti ve ölçüm azına düştü. Etiketle arama büyüyen ekranda hâlâ tek adımdır. Ekran büyüdükçe metinle aramanın aşımı artar; etiketle aramanın aşımı konumdan bağımsızdır.

TA2. Model öneri düğümlerini toplamın önüne koyar; gerçek bir ekranda yeni düğümlerin ağaçtaki yeri ekran tasarımına bağlıdır. Eşlemin her düğümü kapsadığı da varsayılır: iki düğüm aynı etiketi taşırsa gerçek araç hata verir, model vermez.

Özet

  • Bu dersin aşımı: çoğunda. Düğümü metniyle arayan test, sekiz aramanın beşinde ağacın ilk beş adımını geçti.
  • Dersin ölçütü, bir düğümü bulmak için atılan adımın bütçeyi kaç aramada aştığı: aşımı üreten düğümün metni değil, ağaçtaki konumudur.
  • Test etiketi ve eşlem üzerinden arama her örnekte tek adımdır; aynı ekranda çoğundadan içinde yönüne geçildi. Eşlem ekran başına bir kez kurulur.
  • Ekran sekiz düğüm büyüyünce metinle arama 12 adımlık bütçede içindeden azındaya geçti, etiketle arama içinde kaldı.
  • Kural adımı sayar, düğümün kullanıcıya doğru görünüp görünmediğine bakmaz; etiketle arayan test yanlış yazılmış bir düğme metnini yakalamaz.

Sonraki Adım

Bu derste ekran duruyordu ve arama yalnız adım saydı. Gerçek bir ekran ise hazır olmadan test onu aramaya başlayabilir. Sıradaki ders, Test Kararlılığı, hazır olma turunu bekleyen bir testin bekleme bütçesini kaç koşumda aştığını ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat