Ders 03 / 19
Aygıt Üstü Testler
Aygıt üstü test gerçek bir ekranı bir aygıtta ya da öykünücüde çalıştırır ve her testin kurulumunu yeniden öder. Ders kurulum işinin bütçeyi kaç testte aştığını ve kurulumu paylaşmanın bedelini ölçer.
Yayın Güncelleme
Bu derste
- Aygıt üstü testi birim testinden, gerçek aygıtı öykünücüden ayırır
- Her testte yeniden ödenen kurulum işinin bütçeyi kaç testte aştığını sınıflar
- Kurulumu test sınıfı başına bire indirmenin aşımı nasıl değiştirdiğini ölçer
- Testler arasında paylaşılan durumun sonraki testlerin başlangıcını nasıl bozduğunu gösterir
İçindekiler
Önceki derste sahte bağımlılıklar ağın yerini aldı ve testin hiçbir parçası bir aygıtta koşmadı. Güdük kendi yanıtını ölçtürüyordu; gerçek bir ekranın gerçek bir sistemle nasıl konuştuğunu ölçtürmüyordu. Bunu ancak ekranı gerçekten çalıştıran bir test söyler. Bu dersin sorusu kursun sorusudur: bu ölçüm bütçesini kaç örnekte aşıyor? Dört yanıt aynıdır: içinde, azında, çoğunda, hepsinde. Ölçülen iş birimi, bir testin başlamadan önce ve çalışırken yaptığı iştir.
Çekirdek önceki derslerle aynıdır: Budget bir sınırı, Overrun bir ölçümün örneklerini ve bunların kaçının sınırı kesin olarak aştığını taşır.
// 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 }}")
}
}
Aygıt Üstü Test Neyi Çalıştırır
Aygıt üstü test (instrumented test), uygulamayı ve test kodunu bir Android aygıtına yükler ve oradaki sistemle birlikte çalıştırır. Birim testi geliştirme makinesinde, saf Kotlin katmanında koşar; aygıt üstü test ise gerçek bir ekranı, gerçek bir yaşam döngüsünü ve sistemin kendi hizmetlerini kullanır. Bu, birim testinin yakalayamadığı kusurları yakalar: bir ekranın döndürüldüğünde durumunu kaybetmesi, bir izin iletişim kutusunun açılmaması, bir veri tabanının sistemde gerçekten oluşmaması.
Testlerin koştuğu yer iki türlüdür. Gerçek aygıt (physical device) bir kullanıcının eline geçecek donanımdır; sensör, kamera ve üreticiye özgü sistem davranışı yalnız orada doğrulanır. Öykünücü (emulator) bir aygıtın yazılımla taklit edilmiş hâlidir; kurması ve silmesi kolaydır, ama donanım davranışını yalnız taklit eder. Her ikisinde de bir test çalıştırıcısı (test runner) uygulamayı yükler, testleri sırayla koşar ve sonucu toplar.
Bu çalışma düzeninin bir bedeli var: her test, sınadığı ekranı kendi başına ayağa kaldırmak zorundadır. Test bir ekranı başlatır, gerekli veriyi yükler, kullanıcıyı oturum açmış duruma getirir ve ancak sonra asıl iddiasına geçer. Bu hazırlığa test düzeneği (fixture), hazırlığın yaptığı işe ise fixtureWork diyoruz. Platformda bir ekranı sıfırdan başlatan test şöyle yazılır.
// Platform kodu: CartScreenTest
@RunWith(AndroidJUnit4::class)
class CartScreenTest {
@Test
fun showsEmptyCart() {
ActivityScenario.launch(CartActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
assertEquals(0, activity.itemCount())
}
}
}
}
launch çağrısı her testte ekranı yeniden başlatır; use bloğu test biterken onu kapatır. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde başlatma işi sayılır, bir ekran gerçekten başlatılmaz.
TA1. Model, aygıt üstü testi koşturmaz; bu ortamda aygıt ya da öykünücü yoktur. Kurulum işi adımların toplamıdır: ekranı başlatmak 6, tohum veriyi yüklemek 4, oturum açmak 2. Bu sayılar modelin girdisidir, bir aygıtın ölçümü değildir.
Her Testte Ödenen Kurulum
Model iki işlev kurar. İlki her testte ekranı sıfırdan başlatır: testin işi, kurulum işi ile testin kendi gövdesinin işinin toplamıdır. İkincisi kurulumu test sınıfı başına bir kez yapar: yalnız ilk test kurulumu öder, sonrakiler ekranı hazır bulur.
// Fixture.kt — ekrani sifirdan baslatan test duzeni: kurulum isi adimlarla sayilir
class Fixture(val steps: List<Int>) {
val fixtureWork: Int = steps.sum()
}
fun freshPerTest(fixture: Fixture, bodies: List<Int>): List<Int> =
bodies.map { fixture.fixtureWork + it }
fun sharedPerClass(fixture: Fixture, bodies: List<Int>): List<Int> =
bodies.mapIndexed { index, body -> (if (index == 0) fixture.fixtureWork else 0) + body }
Her iki işlev de bir test gövdesi listesinden bir iş listesi üretir; Overrun’a verilecek örnekler bu listelerdir. Sekiz test düşünelim ve gövdelerinin işi 1, 2, 1, 3, 2, 1, 2 ve 3 olsun. Bütçe, bir testin yapabileceği işi 10 birimle sınırlar. Bu sayı bir platform sınırı değildir; ekibin bir testin kısa kalması için koyduğu hedefin modeldeki karşılığıdır.
// Main.kt
fun main() {
val fixture = Fixture(listOf(6, 4, 2))
val bodies = listOf(1, 2, 1, 3, 2, 1, 2, 3)
println("kurulum isi: ${fixture.fixtureWork}")
val work = freshPerTest(fixture, bodies)
println("test basina is: $work")
val overrun = Overrun("her-testte-kurulum", Budget("is", 10), work)
overrun.line()
countOverruns(listOf(overrun))
}
kurulum isi: 12 test basina is: [13, 14, 13, 15, 14, 13, 14, 15] her-testte-kurulum: butce=is<=10 | asan=8/8 -> hepsinde icinde: 0 azinda: 0 cogunda: 0 hepsinde: 1
Kurulum işi tek başına 12 birimdir ve 10 birimlik bütçenin üstündedir. Gövdeler ne kadar küçük olursa olsun sekiz testin sekizi bütçenin üstüne çıktı: ölçüm hepsindedir. Hepsinde burada bir ipucu verir: aşımın nedeni hiçbir testin gövdesi değildir. En küçük gövde 1 birimdir ve yine 13’e ulaşır. Neden kurulumdur ve her testte yeniden ödenir.
Bütçeyi gevşetmek aşımı kapatırdı: 16 birimlik bir bütçe sekiz testi de içinde bırakırdı. Ama bu, kurulumun pahalı olduğu gerçeğini değiştirmez; yalnız onu ölçmenin dışına iter. Kural aşımın büyüklüğünü görmez ve kurulumun neden pahalı olduğunu da görmez. Bunu düzyazı söylemelidir.
Kurulumun Parçaları
Modeldeki 12 birimlik kurulum üç parçadan oluşur ve her parça gerçek bir testte ayrı bir iş karşılığıdır. Ekranı başlatmak, sistemden bir ekranın istenmesi, onun yaşam döngüsünün ilk durumlara getirilmesi ve çizilmesidir; bu yüzden üç parçanın en pahalısı olan 6 birimi alır. Tohum veriyi yüklemek, ekranın göstereceği kayıtların veri tabanına yazılmasıdır ve 4 birimdir. Oturum açmak, kullanıcıyı ekranın beklediği duruma getirir ve 2 birimdir.
Bu üç parçanın hiçbiri testin asıl iddiasına ait değildir. Test “sepet boşken boş mesajı görünür” diyorsa, iddia tek bir satırdır. Önceki üç iş yalnız iddiayı mümkün kılar. Bu yüzden kurulum işi test gövdesinden ayrı bir sütunda sayılır.
Kurulumu küçültmenin iki yolu vardır. Birincisi parçaları ucuzlatmaktır: tohum veriyi daha az kayıtla yüklemek ya da oturumu sahte bir kimlik doğrulayıcıyla vermek. İkincisi parçaları paylaşmaktır; sınıf başına bir kez yapmak bunun bir biçimidir. Birincisi testin gerçekçiliğinden fedakârlık eder, ikincisi testlerin bağımsızlığından.
Kaynak Parçaların Satır Satır Okunuşu
CartScreenTest parçasında @RunWith(AndroidJUnit4::class) satırı, testin aygıt üstü bir çalıştırıcıyla koşacağını bildirir. ActivityScenario.launch(CartActivity::class.java) ekranı, yani activity’yi sistemin gerçekten başlatmasını ister ve bir senaryo nesnesi döndürür. .use { scenario -> ... } bloğu, senaryo ne olursa olsun blok bittiğinde onu kapatır; kapatılmayan bir ekran sonraki testi etkiler. scenario.onActivity { activity -> ... } bloğu, activity’ye ekranın kendi iş parçacığında erişim verir. assertEquals(0, activity.itemCount()) iddianın kendisidir: sepette sıfır ürün var.
Sınırlayıcı ölçümde göreceğimiz SeedOnce parçasında companion object Kotlin’de sınıfa bağlı üyeleri tutar. @BeforeClass ek açıklaması, yöntemin sınıftaki ilk testten önce bir kez çalışmasını ister. @JvmStatic, yöntemin çalıştırıcının beklediği biçimde, gerçek bir statik yöntem olarak üretilmesini sağlar. seedDatabase() ise kurulumun pahalı parçasıdır ve modelde 4 birimlik tohum yüklemeye karşılık gelir.
Gerçek Aygıt mı, Öykünücü mü
Bütçeyi kuran taraf testlerin koştuğu yeri de seçmek zorundadır. Öykünücü hızlı kurulur ve tek komutla sıfırlanabilir; sürekli tümleştirme hattında bu, her koşumun temiz bir durumdan başlaması demektir. Gerçek aygıt ise donanıma bağlı kusurları yakalar, ama her koşumda temiz bir durum garanti etmez. Önceki testin bıraktığı bir izin ya da bir hesap aygıtta kalmış olabilir.
Bu derste ölçülen kurulum işi bu seçimden bağımsızdır. İkisinde de ekran başlatılır, veri yüklenir ve oturum açılır. Değişen, işin birim başına maliyetidir, ve model maliyeti süreyle değil adımla sayar.
Test Piramidi ve Bütçe
Aygıt üstü testlerin pahalılığı, test paketinin biçimini belirler. Birim testi hızlıdır ve çok sayıda koşar; aygıt üstü test yavaştır ve az sayıda koşar. Bu dağılıma test piramidi denir: tabanda çok sayıda birim testi, ortada bileşenleri bir araya getiren testler, tepede az sayıda uçtan uca aygıt üstü test bulunur. Piramidi ters çeviren, yani her şeyi aygıt üstünde sınayan bir paket, bu derste ölçtüğümüz kurulum maliyetini binlerce testle çarpar.
Ekip bütçeyi her test için ayrı koyar. Birim testinin bütçesi birkaç adım olabilir; aygıt üstü testin bütçesi kuruluma göre belirlenir. Aynı 10 birim, iki farklı test türünde çok farklı bir beklentidir. Bu yüzden kurs boyunca her ölçümün bütçesi yanında bir gerekçeyle yazılır.
Paylaşılan Kurulumun Koşulları
Kurulumu sınıf başına paylaşmak, bazı koşullara bağlıdır. Testlerin sırası sonucu etkilememelidir; bir test diğerinin bıraktığı durumdan yararlanıyorsa, sırası değiştiğinde ya da tek başına koştuğunda kırılır. Paylaşılan durumu değiştiren testler, işlerini bitirdiklerinde durumu eski hâline getirmelidir. Platformda bu, her testten sonra çalışan bir temizlik yöntemiyle yapılır. Son olarak, paylaşılan kurulum salt okunur olmalıdır: tohum veri bir kez yüklenir ve hiçbir test onu değiştirmez.
Bu üç koşuldan biri sağlanamıyorsa, kurulumu paylaşmak hızı artırır ama kararlılığı düşürür. Sepet ölçümü bu dengeyi gösterdi: paylaşılan sepette yedi test başlangıçta bir şey buldu. Temizliğin de bir bedeli vardır ve her testten sonra yapılan temizlik, kurulumun yarattığı kazancın bir kısmını geri alır.
Yaygın Yanılgılar
İlk yanılgı, “aygıt üstü test birim testinin yerini alır” düşüncesidir. Aygıt üstü test gerçek sistemi kullanır ve bu yüzden daha pahalıdır. Bir işlevin sınır durumlarını aygıt üstünde sınamak, kurulumu her sınır için yeniden ödemek demektir. Sınır durumları birim testinde, ekran akışları aygıt üstünde sınanır.
İkinci yanılgı, “kurulumu paylaşmak her zaman iyidir” düşüncesidir. Paylaşılan kurulum ilk testin yükünü azaltmaz, yalnız bir kez ödetir; ama paylaşılan durum testleri birbirine bağlar. Sepet ölçümünde yedi testin başlangıçta bir şey bulması bunun somut göstergesiydi.
Üçüncü yanılgı, “bütçeyi aşan testi bütçeyi büyüterek kapatmak yeterlidir” düşüncesidir. Bütçeyi 16’ya çıkarmak ölçümü hepsindeden içine taşırdı, ama kurulumun maliyeti hiç değişmezdi. Bütçe ürün kararına dayanır; aşımı kapatmak için değiştirilen bir bütçe ölçümün anlamını yok eder.
Sınırlayıcı Ölçüm
Bu ölçümün sınırlayıcısı sınıf başına kuruluma geçmektir. Gerekçesi şudur: ilk ölçümde aşımın nedeni kurulumdu ve bunu doğrulamak için tek yapılacak şey kurulumun ödendiği yeri değiştirmektir. Gövdeler, bütçe ve örnek sayısı aynı kalır. Aşım hepsindeden azına geçerse, neden gerçekten kurulumdu.
Kurulumu test sınıfı başına bir kez yapalım. Testlerin gövdesi, bütçe ve kurulum aynı kalıyor; yalnız kurulumun ne zaman ödendiği değişiyor. Platformda bu, sınıf başına bir kez çalışan bir hazırlık yöntemiyle yapılır.
// Platform kodu: SeedOnce
class CartScreenTest {
companion object {
@BeforeClass
@JvmStatic
fun seedOnce() {
seedDatabase()
}
}
}
Hazırlık yöntemi sınıftaki ilk testten önce bir kez çalışır. Modelde bu, kurulum işinin yalnız ilk testte ödenmesidir.
// Main.kt — kurulum sinif basina bir kez odenir
fun main() {
val fixture = Fixture(listOf(6, 4, 2))
val bodies = listOf(1, 2, 1, 3, 2, 1, 2, 3)
val work = sharedPerClass(fixture, bodies)
println("test basina is: $work")
val overrun = Overrun("sinif-basina-kurulum", Budget("is", 10), work)
overrun.line()
countOverruns(listOf(overrun))
}
test basina is: [13, 2, 1, 3, 2, 1, 2, 3] sinif-basina-kurulum: butce=is<=10 | asan=1/8 -> azinda icinde: 0 azinda: 1 cogunda: 0 hepsinde: 0
Aynı sekiz test, aynı bütçe. Yedi test 1 ile 3 birim arasında kaldı, yalnız ilk test 13’e çıktı. Ölçüm hepsindeden azına geçti. Aşım tamamen kapanmadı: kurulum yok olmadı, yalnız tek bir testin üstüne yığıldı. Kural bunu “bir test aşıyor” diye okur; ilk testin kurulumun yükünü taşıdığını yine düzyazı söylemelidir.
Bu kazanç bedelsiz değildir. Kurulum paylaşılınca testler aynı ekranı, aynı veriyi ve aynı sistem durumunu paylaşır. Bir test sepete bir ürün koyarsa, sonraki test sepeti dolu bulur. Bunu bir ölçümle görelim: her test sepete belirli sayıda ürün ekliyor ve sonraki test başlarken sepette ne kadar ürünün kaldığı sayılıyor. Bütçe sıfırdır, çünkü her test boş bir sepetle başlamalıdır.
// Main.kt — paylasilan durum sonraki testin baslangicini bozar
fun leftoverBefore(adds: List<Int>, cleanup: Boolean): List<Int> {
val cart = mutableListOf<String>()
return adds.map { count ->
val leftover = cart.size
repeat(count) { cart.add("urun") }
if (cleanup) cart.clear()
leftover
}
}
fun main() {
val adds = listOf(1, 0, 2, 1, 0, 1, 1, 0)
val shared = leftoverBefore(adds, false)
val cleaned = leftoverBefore(adds, true)
println("temizlik yok: $shared")
println("temizlik var: $cleaned")
val clean = Budget("kalan", 0)
val overruns = listOf(
Overrun("paylasilan-sepet", clean, shared),
Overrun("temizlenen-sepet", clean, cleaned),
)
overruns.forEach { it.line() }
countOverruns(overruns)
}
temizlik yok: [0, 1, 1, 3, 4, 4, 5, 6] temizlik var: [0, 0, 0, 0, 0, 0, 0, 0] paylasilan-sepet: butce=kalan<=0 | asan=7/8 -> cogunda temizlenen-sepet: butce=kalan<=0 | asan=0/8 -> icinde icinde: 1 azinda: 0 cogunda: 1 hepsinde: 0
Paylaşılan sepet çoğundadır: ilk test dışında yedi test başlangıçta bir şey bulur. Her testten sonra temizleme yapılırsa ölçüm içindedir, ama temizleme de iştir. Kurulumu paylaşmak testi hızlandırır; paylaşılan durumu temizlemek bu kazancın bir kısmını geri alır. Hangi yöntemin seçileceği, ekibin testlerin bağımsızlığına mı yoksa süresine mi öncelik verdiğine bağlıdır. Kural bu kararı vermez, yalnız kararın sonucunu sayar.
TA2. Model sepeti bir liste olarak kurar ve temizlemeyi tek adımda yapar. Gerçek bir veri tabanında temizleme, tablo sayısı ve yabancı anahtarlara göre değişen bir işlemdir; modelde bu fark yok.
Özet
- Bu dersin aşımı: hepsinde. Her test ekranı sıfırdan başlattığı için 12 birimlik kurulum sekiz testin sekizinde 10 birimlik bütçeyi aştırdı.
- Dersin ölçütü, bir testin işinin bütçeyi kaç testte aştığı: her testte kurulumla hepsinde, sınıf başına kurulumla azında çıktı.
- Aşımın nedeni test gövdesi değildi; en küçük gövde 1 birim olduğu hâlde test 13’e çıktı. Neden kurulumdu ve her testte yeniden ödeniyordu.
- Kurulumu paylaşmak ilk teste yükü yığdı ve aşım hepsindeden azına geçti, ama kurulum yok olmadı.
- Paylaşılan durum sonraki testlerin başlangıcını bozdu: temizlik yokken ölçüm çoğunda, her testten sonra temizlikle içinde çıktı.
Sonraki Adım
Bu derste ekran sıfırdan başlatıldı ve test içinde ekranın hangi parçasının kontrol edileceği hiç sorulmadı. Gerçek bir arayüz testi ekranda bir düğümü bulmak zorundadır ve bulmanın bir yolu vardır. Sıradaki ders, Arayüz Testleri, bir düğümü metniyle aramanın ve test etiketiyle aramanın bütçeyi kaç örnekte aştığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.