TestDispatcher: Saat Ustası Olun | yazan Michał Klimczak | Haz, 2022

TestDispatcher: Saat Ustası Olun |  yazan Michał Klimczak |  Haz, 2022

Kotlin eşyordamlarını test etmenin inceliklerine derinlemesine bir dalış.

fotoğrafı çeken Matteo Vella üzerinde Sıçramayı kaldır

Kotlin eşyordamlarında akıcı olsanız bile, bunları test etmekte yine de zorlanabilirsiniz. Eşzamanlılık, özellikle testte gerekli olan %100 deterministik davranışı hedefliyorsanız, doğası gereği akıl yürütmesi zordur.

Kotlin 1.6, eşyordam test ortamına birçok değişiklik getirdi. runTest adlandırma kuralının basit bir değişikliği değildir runBlockingTest, testlerin nasıl çalıştığına dair temel değişiklikler yapıldı. Bu, 1.5 ve 1.6 arasındaki değişikliklerle ilgili başka bir makale olmayacak, ancak son zamanlarda bunlardan bolca var. Bunun yerine, çok açık olmayan birkaç tanesine odaklanacağız. TestDispatcher hayal kırıklığı içinde saçınızı çekmenize neden olabilecek özellikler.

Yani, bu makalede bulacaksınız bazı teori:

  • Planlanan ve yürütülen bir görev arasındaki fark nedir?
  • ana özellikleri nelerdir StandardTestDispatcher ve UnconfinedTestDispatcher ve nasıl farklılar?
  • Bir TestDispatcher’ın sanal zamanını (görselleştirme ile) nasıl kontrol edebilirsiniz?

Ve kodda açıklanan ince TestDispatcher tuzakları:

  • Hiç bitmeyen eşyordamlar testin bitmesini nasıl önleyecek.
  • Nasıl UnconfinedTestDispatcher testlerinizi beklenmedik bir şekilde karıştırabilir.
  • Kullanarak fazladan 1 ms’ye nasıl ihtiyacınız olabilir? advanceTimeBy.

Lütfen bu makalenin Kotlin 1.7.0’daki işlerin durumuna dayandığını unutmayın. Burada kullanılan API’lerin çoğu hala @ExperimentalCoroutinesApi bu nedenle gelecekte değişebilirler. Ayrıca, kullanacağız türbin ve alaycı Bu örneklerde test durumlarını basitleştirmek için.

Pratik örneklere geçmeden önce, neyin ne olduğunu anladığımızdan emin olalım. TestDispatcher ve neden eşyordam testinin kalbidir.

TestDispatcher özel bir durumdan başka bir şey değilCoroutineDispatcher(yani kuzeni Dispatchers.Main, Dispatchers.IO, vb.). Diğer herhangi bir gönderici gibi, bir CoroutineScope ve onun işi eşyordamların yürütülmesini düzenlemek bu kapsamda başlatıldı. Aradaki fark, herhangi bir normal göndericiden farklı olarak, bu düzenlemeyi aracılığıyla kontrol etmemizdir. TestCoroutineSchedulersanal zamanı ve zamanlanmış görevlerin manuel olarak yürütülmesi.

Eşyordamların zamanlanması ve yürütülmesi

TestDispatchers’ın nasıl çalıştığını anlamama gerçekten yardımcı olan bir şey, bir görev arasındaki farkı anlamaktı. planlanmış ve uygulanmış. Üretim kodunda bu ayrım o kadar önemli değildir çünkü her şey gerçek bir saatte gerçekleşir ve çoğu durumda zamanlanmış görevler hemen yürütülür. TestDispatchers ile fark belirginleşir. Temel olarak, tam olarak bu milisaniyede yürütülmesi planlanan bir görevi düşünüyorsak bile, bu, abonelerinin onu hemen alacağı anlamına gelmez. tarafından yürütülmesi gerekmektedir. CoroutineDispatcher ilk. Hızlı bir bakış TestCoroutineScheduler kaynak çok şey ortaya çıkarır:

public class TestCoroutineScheduler {

/** This heap stores the knowledge about which dispatchers are interested in which moments of virtual time. */
private val events = ThreadSafeHeap<TestDispatchEvent<Any>>()

/** Establishes that [currentTime] can't exceed the time of the earliest event in [events]. */
private val lock = SynchronizedObject()

/** This counter establishes some order of the events that happen at the same virtual time. */
private val count = atomic(0L)

Bu 3 özellik bize şunları söylüyor:

  • events: Sevkiyat görevlileri kayıt olmak için zamanlayıcıyı kullanır Etkinlikler — ilgilendikleri belirli anlar.
  • count: Aynı sanal zamanda birden fazla olay planlanmış olsa bile, onların deterministik sırasını sağlamak için bir mekanizma vardır (her ne kadar tarafından kullanılmasa da). UnconfinedTestDispatcher).
  • lock: Sanal saat hareket ettirilirse garanti edilir. geçmiş görev için zamanlanan zaman — bu görev yürütülür.

Bunların hepsi aşağıdaki resimlerde anlaşılması daha kolay hale gelecektir.

StandardTestDispatcher vs UnconfinedTestDispatcher

Testlerde kullanılan yalnızca iki test dağıtıcısı vardır. StandardTestDispatcher her kullandığımızda sağlanan varsayılan değerdir runTest. Görevlerin yürütülme sırası konusunda kesin garantileri vardır, ancak yürütme istekli değildir, yani kullanmamız gerekir. runCurrent sanal zamanın geçerli anında tetiklemek veya ile zamanı manuel olarak ilerletmek için advanceTimeBy ve advanceUntilIdle.

UnconfinedTestDispatcher heveslidir, dürtme gerektirmezrunCurrent yürütmek için sopa. Sanal zamanı otomatik olarak ilerletir ve sıraya alınan tüm görevleri yürütür. Ancak dezavantajı, içinde programlanan birkaç eşyordamın sırasını garanti etmeyecek olmasıdır. Temelde gibi çalışır Dispatchers.Unconfined, ancak otomatik olarak gelişmiş sanal zamanla. Bu, aşağıda açıklanacağı gibi, çok fazla kafa karışıklığına neden olabilir.

Sanal saat ibresini hareket ettirme

İle birlikte StandardTestDispatcherprogramlanmış eşyordamların yürütülmesini 3 yöntemden biriyle hassas bir şekilde kontrol edebiliriz:

  • runCurrent: sanal zamanın geçerli anında zamanlanan tüm görevleri yürütür.
  • advanceTimeBy: sanal zamanı verilen milisaniye miktarı kadar ilerletir ve bu arada planlanan görevleri yürütür.
  • advanceUntilIdle: benzer şekilde çalışıradvanceTimeByancak sanal zamanı belirli bir milisaniye kadar ilerletmek yerine, kuyrukta zamanlanmış başka görev kalmayana kadar ilerletir.

Şimdi bu sanal zaman çizelgesini görselleştirelim. Diyelim ki, planlanmış 4 görevimiz var:

A: 0 ms’de programlanır (hemen).
B: 1000 ms’de planlandı.
C: 1000 ms’de programlanır, ancak B’den sonra kaydedilir.
D: 2000 ms’de planlandı.

Her yöntemin zaman çizelgesini nasıl etkileyeceğini görelim.

runCurrent() sanal zamanı hareket ettirmeyecek, ancak görevi yürütecek Ageçerli zamanda (0 ms) programlanır.

advanceTimeBy(1000) saati 1000 ms hareket ettirecek ve görevi yürütecek A, bu arada planlandı. Ancak yürütülmeyecek B ve C henüz.

Bunları yürütmek için açıkça çağırmamız gerekir. runCurrent() sonrasında advanceTimeBy(). Bu, aşağıdaki örneklerden birinde kodda gösterilecektir. Lütfen unutmayın, eğer B daha önce kayıtlıydı Cbu emir tarafından yürütülmek üzere muhafaza edilecektirStandardTestDispatcher. UnconfinedTestDispatcher bunu garanti etmez.

Ve sonunda, yapabiliriz advanceUntilIdle()bu da süreyi 2000 ms – yani şu anda zamanlanmış olan tüm görevler yürütülene kadar ilerletecektir.

Artık temel bilgileri aradan çıkardığımıza göre, hadi epeyce yetişkin erkeğin bilgisayar başında çığlık atmasına neden olan birkaç inceliğe bir göz atalım.

Hiç bitmeyen eşyordamlar testin bitmesini engelleyecektir

Ses çalmayı başlatan bir MediaPlayer sınıfını ele alalım.

Bazen, sesi çalması gereken eşyordam, muhtemelen bozuk bir dosya nedeniyle bir istisna atar. MediaPlayer müşterilerini bu hatalar hakkında bilgilendirecek ve bu davranışı test edebiliriz.

Çok uzak çok iyi. Şimdi bu hataları UI katmanına ulaştırmanın yanı sıra Crashlytics’e de bildirmek istediğimizi ve özel bir IssueReporter halletmek için sınıf. sadece gözlemleyebiliriz playerErrors Akış ve bunları rapor edin.

Bu test bir dakika boyunca çalışacak ve ardından aşağıdakilerle zaman aşımına uğrayacaktır:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs: ["coroutine#4":StandaloneCoroutine{Active}@36790bec]

Nedenmiş? Çünkü test kapsamında çalışan bir eşyordam işini sızdırdık – açıkçası playerErrors.collect. Test dağıtıcıları, aktif bir eşyordam varsa,runTest tüm alt öğeleri bitene kadar (veya varsayılan olarak 60 s olan ve ile değiştirilebilen zaman aşımına kadar) engelleyecektir. runTest(dispatchTimeoutMs=x)).

Bu aslında oldukça kullanışlı: Bir kaynağı sızdırdık, testler bize bundan bahsetti ve şimdi onu temizlememiz gerekiyor. O halde bir ekleyelim dispose() kapsamı iptal ederek bununla ilgilenecek yöntem. Bu düzeltmeli, değil mi?

Hayır. Evet, sızıntı düzeltildi, ancak test hala başarısız oluyor:

TestScopeImpl was cancelled
kotlinx.coroutines.JobCancellationException: TestScopeImpl was cancelled; job=TestScope[test ended]

Test kapsamları iptal edilmekten hoşlanmaz. Bizim yapabileceğimiz, huzur içinde ölebilmesi için çocuklarının işlerinin iptal edilmesini sağlamaktır. değiştirirsek sut.dispose() ile birlikte this.coroutineContext.cancelChildren() sınav geçecek.

Şahsen, bu kod satırını çok açıklayıcı bulmuyorum, bu yüzden okuyucunun neler olduğunu bilmesi için onu bir işleve sarmayı seviyorum.

private fun TestScope.cancelNeverEndingCoroutines() = this.coroutineContext.cancelChildren()

Bu davranışın biraz tartışmalı olduğunu unutmayın, bu nedenle gelecekte değişmesi olasıdır. Örneğin https://github.com/Kotlin/kotlinx.coroutines/issues/1531.

UnconfinedTestDispatcher, birleşik StateFlow emisyonlarını karıştıracak

Bu aslında sürpriz olmamalı çünkü belgelenmişBununla birlikte, belirsizliğin doğası UnconfinedTestDispatcher (ve orijinal UnconfinedDispatcher çok, fwiw) oldukça ince olabilir. Hepimizi kurtardığı için bazen yararlıdır. runCurrent arar ama zaman zaman yüzümüze patlayabilir.

hadi kullanalım MediaPlayer yine örnek.

Bir üretim uygulamasında çalıştırılırsa, bu kod parçası düzgün şekilde yayacaktır Playingardından sesi çalın ve ardından Stopped. Bunu test edip alay ettiğimizde işler biraz garipleşiyor playSound 0 ms eşyordamı olmak.

Oyuncu durumunun değişmesini bekliyoruz Playing. Ancak bu test 60 saniye sonra zaman aşımına uğrayacak – ikinci awaitItem() sonsuza kadar askıya alacaktır. Nedenmiş?

Birleştirilmiş iki nedenden dolayı:

  • StateFlow emisyonların birleştirilmesine izin verilir – örneğin iki özdeş değeri kabul ederse (iki Stopped birbiri ardına), yalnızca bir kez yayılacaktır.
  • UnconfinedTestDispatcheratıfta bulunarak dokümanlarbu göndericide birkaç eşyordam sıraya alındığında yürütme emri hakkında garanti vermez”.

Artık kapsam dışı bir dispeçer ile çıktık ve içinde 3 askıya alma fonksiyonunu çalıştırdık.

scope.launch {            
playerState.emit(PlayerState.Playing)
soundPlayer.playSound()
playerState.emit(PlayerState.Stopped)
}

Arada çok şey olmasına rağmen sut.play() ve awaitItem()kollektörümüz (Türbin sut.playerState.test) tüm gösteriyi kaçırdı. Hala sadece görüyor PlayerState.Stopped.

Değiştirerek kolayca sabitlenir UnconfinedTestDispatcher ile birlikte StandardTestDispatcherbu da ikincisinin garanti awaitItem() bekleyecek playerState.emit(PlayerState.Playing) ve ancak bundan sonra devam edecek.

Bu örnek, aşağıdakilerle ilgili sorunların olduğunu göstermek için burada UnconfinedTestDispatcher olayları beklemek gibi her zaman çok açık değildir. A-B-C-D sipariş ve alma A-C-B-D. Gibi coroutines makinelerinin geri kalanıyla birlikte StateFlow‘in örtük birleşimi, gerçekten belirsizleşebilir.

TestScope.advanceTimeBy, tam olarak geçerli sanal zamanda planlanan eşyordamları yürütmez, yalnızca daha önce planlanmış olanları yürütür

Bu eski 1.5 ile kafa karıştırıcı bir fark TestCoroutineScope ve yeni 1.6 TestScope. Yenisi yalnızca sanal zamanı hareket ettirecek, ancak bekleyen herhangi bir görevi yürütmeyecektir. TestCoroutineScheduler. Eskisi ayrıca arayacak runCurrent. Bu tabii ki belgelenmiş:

Aksine TestCoroutineScope.advanceTimeBybu işlev şu anda programlanan görevleri currentTime + delayTimeMillis çalıştırmaz.

Uygulamada, bu yeni davranış, daha sonra advanceTimeByaramak zorundayız runCurrent açıkça veya sadece süreyi biraz daha ilerletin (1 ms ile).

Bir örnek görelim.

eski dostumuzla MediaPlayerartmayacağımızdan emin olmak istediğimizi varsayalım playbackCounter oynatma gerçekten bitene kadar. Aşağıdaki test tam da bunu yapmamıza izin verecek:

Sahte ses oynatıcısını 1000 ms çalıştırıyoruz, böylece 500 ms sonra sayacın hala 0 olup olmadığını kontrol edebiliriz. Şimdi, çalmayı bitirdikten sonra gerçekten 1’e değişip değişmediğinden emin olmak için başka bir test hazırlayalım.

Ne yazık ki, bu testin iddiası başarısız olur. Kullanımdan kaldırılanlarla geçerdi runBlockingTest, ancak günümüzde, zamanlanmış görevlerin yürütülmesi konusunda açık olmamız gerekiyor. ekleyerek testi düzeltebiliriz. runCurrent() hemen sonra advanceTimeBy(1000) veya (sanırım biraz daha az zarif) ile değiştirerek advanceTimeBy(1001).

Eşyordam test çerçevesine yönelik birkaç kafa karıştırıcı davranış daha var: Dispatchers.setMain ardışık tüm testler için dolaylı olarak varsayılan bir test dağıtıcısı sağlamak (bunun için ikna edici bir bozuk test hayal edemememe rağmen). Kendiniz kırılgan bir eşyordam testi senaryosu bulduysanız, yorumlarda bana bildirin.

Bununla birlikte, genel olarak, 1.6’daki değişiklikler ileriye doğru atılmış büyük bir adımdır ve test eşzamanlılığını daha öngörülebilir hale getirir. Olmadıkları durumlarda, umarım bu örnekler birinin kafasından biraz daha az saç çekmesine yardımcı olur.

Ek kaynaklar:

Ayrıca teşekkürler Artur Klamborowski Bu makaledeki yardımınız için.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.