Test çiftlerine ve bağımlılık eklemeye giriş

Bu codelab, Advanced Android in Kotlin kursunun bir parçasıdır. Bu kurstan en iyi şekilde yararlanmak için codelab'leri sırayla incelemeniz önerilir ancak bu zorunlu değildir. Kursla ilgili tüm codelab'ler Kotlin'de İleri Düzey Android codelab'leri açılış sayfasında listelenir.

Giriş

Bu ikinci test laboratuvarı, test çiftleriyle ilgilidir: Android'de ne zaman kullanılacağı ve bağımlılık ekleme, hizmet bulucu kalıbı ve kitaplıklar kullanılarak nasıl uygulanacağı. Bu süreçte şunları yazmayı öğreneceksiniz:

  • Depo birimi testleri
  • Parçalar ve ViewModel entegrasyon testleri
  • Parça gezinme testleri

Bilmeniz gerekenler

Aşağıdaki konular hakkında bilgi sahibi olmanız gerekir:

Neler öğreneceksiniz?

  • Test stratejisi planlama
  • Test çiftlerini (sahteler ve taklitler) oluşturma ve kullanma
  • Birim ve entegrasyon testleri için Android'de manuel bağımlılık ekleme nasıl kullanılır?
  • Hizmet Bulucu Kalıbı'nı uygulama
  • Depoları, parçaları, görünüm modellerini ve Navigation bileşenini test etme

Aşağıdaki kitaplıkları ve kod kavramlarını kullanacaksınız:

Yapacaklarınız

  • Test çifti ve bağımlılık ekleme kullanarak bir depo için birim testleri yazın.
  • Test çifti ve bağımlılık ekleme kullanarak bir görünüm modeli için birim testleri yazın.
  • Espresso kullanıcı arayüzü test çerçevesini kullanarak parçalar ve bunların görünüm modelleri için entegrasyon testleri yazın.
  • Mockito ve Espresso kullanarak gezinme testleri yazın.

Bu codelab serisinde, yapılacaklar notları uygulamasıyla çalışacaksınız. Bu uygulama, tamamlanacak görevleri yazmanıza ve bunları bir listede görüntülemenize olanak tanır. Ardından bunları tamamlandı veya tamamlanmadı olarak işaretleyebilir, filtreleyebilir ya da silebilirsiniz.

Bu uygulama Kotlin ile yazılmıştır, birkaç ekranı vardır, Jetpack bileşenlerini kullanır ve Uygulama mimarisi kılavuzundaki mimariye uygundur. Bu uygulamayı nasıl test edeceğinizi öğrenerek aynı kitaplıkları ve mimariyi kullanan uygulamaları test edebilirsiniz.

Kodu İndirme

Başlamak için kodu indirin:

Zip dosyasını indir

Alternatif olarak, kod için GitHub deposunu klonlayabilirsiniz:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1

Aşağıdaki talimatları uygulayarak kodu inceleyin.

1. adım: Örnek uygulamayı çalıştırın

TO-DO uygulamasını indirdikten sonra Android Studio'da açıp çalıştırın. Derlenmesi gerekir. Aşağıdaki adımları uygulayarak uygulamayı keşfedin:

  • Kaydırma artı düğmesiyle yeni bir görev oluşturun. Önce başlık, ardından görevle ilgili ek bilgileri girin. Yeşil onay işaretiyle kaydedin.
  • Görev listesinde, yeni tamamladığınız görevin başlığını tıklayın ve açıklamanın geri kalanını görmek için bu görevin ayrıntılar ekranına bakın.
  • Listede veya ayrıntılar ekranında, görevin durumunu Tamamlandı olarak ayarlamak için görevin onay kutusunu işaretleyin.
  • Görevler ekranına geri dönün, filtre menüsünü açın ve görevleri Etkin ve Tamamlandı durumuna göre filtreleyin.
  • Gezinme çekmecesini açın ve İstatistikler'i tıklayın.
  • Genel bakış ekranına dönün ve gezinme çekmecesi menüsünden Tamamlandı durumundaki tüm görevleri silmek için Tamamlananları temizle'yi seçin.

2. adım: Örnek uygulama kodunu inceleyin

Yapılacaklar uygulaması, popüler Architecture Blueprints test ve mimari örneğini (örneğin reactive architecture sürümünü kullanarak) temel alır. Uygulama, Uygulama mimarisi kılavuzundaki mimariye uygundur. Fragments, bir depo ve Room ile ViewModels kullanır. Aşağıdaki örneklerden herhangi birini biliyorsanız bu uygulamanın mimarisi benzerdir:

Uygulamanın genel mimarisini anlamanız, herhangi bir katmandaki mantığı derinlemesine anlamanızdan daha önemlidir.

Bulacağınız paketlerin özeti:

Paket: com.example.android.architecture.blueprints.todoapp

.addedittask

Görev ekleme veya düzenleme ekranı: Görev ekleme veya düzenleme için kullanıcı arayüzü katmanı kodu.

.data

Veri katmanı: Görevlerin veri katmanıyla ilgilidir. Veritabanı, ağ ve depo kodunu içerir.

.statistics

İstatistikler ekranı: İstatistikler ekranı için kullanıcı arayüzü katmanı kodu.

.taskdetail

Görev ayrıntıları ekranı: Tek bir görev için kullanıcı arayüzü katmanı kodu.

.tasks

Görevler ekranı: Tüm görevlerin listesi için kullanıcı arayüzü katmanı kodu.

.util

Yardımcı sınıflar: Uygulamanın çeşitli bölümlerinde kullanılan paylaşılan sınıflar (ör. birden fazla ekranda kullanılan yenilemek için kaydırma düzeni).

Veri katmanı (.data)

Bu uygulama, remote paketinde simüle edilmiş bir ağ oluşturma katmanı ve local paketinde bir veritabanı katmanı içerir. Bu projede, basitlik için ağ katmanı gerçek ağ istekleri oluşturmak yerine yalnızca gecikmeli bir HashMap ile simüle edilir.

DefaultTasksRepository, ağ katmanı ile veritabanı katmanı arasında koordinasyon veya aracılık yapar ve verileri kullanıcı arayüzü katmanına döndürür.

Kullanıcı arayüzü katmanı ( .addedittask, .statistics, .taskdetail, .tasks)

Kullanıcı arayüzü katmanı paketlerinin her biri, bir fragment ve bir görünüm modelinin yanı sıra kullanıcı arayüzü için gereken diğer sınıfları (ör. görev listesi için bir bağdaştırıcı) içerir. TaskActivity, tüm parçaları içeren etkinliktir.

Gezinme

Uygulamanın gezinme özelliği Navigation bileşeni tarafından kontrol edilir. Bu değer nav_graph.xml dosyasında tanımlanır. Gezinme, Event sınıfı kullanılarak görünüm modellerinde tetiklenir. Görünüm modelleri, hangi bağımsız değişkenlerin iletileceğini de belirler. Parçalar Event öğesini gözlemler ve ekranlar arasında gerçek gezinmeyi gerçekleştirir.

Bu codelab'de, test çiftlerini ve bağımlılık eklemeyi kullanarak depoları test etmeyi, modelleri ve parçaları görüntülemeyi öğreneceksiniz. Bu testlerin neler olduğunu incelemeden önce, bu testleri ne şekilde yazacağınızı belirleyecek olan mantığı anlamanız önemlidir.

Bu bölümde, Android'de geçerli olan genel testlerle ilgili bazı en iyi uygulamalar ele alınmaktadır.

Test Piramidi

Test stratejisi hakkında düşünürken üç ilgili test yönü vardır:

  • Kapsam: Test, kodun ne kadarını etkiliyor? Testler tek bir yöntemde, uygulamanın tamamında veya ikisinin arasında bir yerde çalıştırılabilir.
  • Hız: Test ne kadar hızlı çalışır? Test hızları milisaniyelerden birkaç dakikaya kadar değişebilir.
  • Doğruluk: Test ne kadar "gerçek dünya" ile ilgili? Örneğin, test ettiğiniz kodun bir bölümünün ağ isteğinde bulunması gerekiyorsa test kodu gerçekten bu ağ isteğinde bulunuyor mu yoksa sonucu taklit mi ediyor? Test ağ ile gerçekten iletişim kuruyorsa bu, testin daha yüksek doğruluk oranına sahip olduğu anlamına gelir. Bununla birlikte, testin çalışması daha uzun sürebilir, ağ bağlantısı kesilirse hatalara neden olabilir veya kullanımı maliyetli olabilir.

Bu yönler arasında doğal olarak ödünler verilir. Örneğin, hız ve doğruluk arasında bir denge vardır. Test ne kadar hızlı olursa doğruluk o kadar düşük olur ve bunun tersi de geçerlidir. Otomatik testleri bölmenin yaygın bir yolu, bunları şu üç kategoriye ayırmaktır:

  • Birim testleri: Bunlar, tek bir sınıfta (genellikle o sınıftaki tek bir yöntemde) çalışan, oldukça odaklanmış testlerdir. Bir birim testi başarısız olursa kodunuzda sorunun tam olarak nerede olduğunu öğrenebilirsiniz. Gerçek dünyada uygulamanız tek bir yöntemin veya sınıfın yürütülmesinden çok daha fazlasını içerdiğinden bu testlerin doğruluğu düşüktür. Kodunuzu her değiştirdiğinizde çalıştırılacak kadar hızlıdırlar. Bu testler genellikle yerel olarak çalıştırılan testlerdir (test kaynak grubunda). Örnek: Görünüm modellerinde ve depolarda tek yöntemleri test etme.
  • Entegrasyon testleri: Bunlar, birlikte kullanıldıklarında beklendiği gibi davrandıklarından emin olmak için çeşitli sınıfların etkileşimini test eder. Entegrasyon testlerini yapılandırmanın bir yolu, tek bir özelliği (ör. görev kaydetme özelliği) test etmelerini sağlamaktır. Entegrasyon testleri, birim testlerine kıyasla daha geniş bir kod kapsamını test eder ancak tam doğruluk yerine hızlı çalışacak şekilde optimize edilir. Duruma bağlı olarak yerel olarak veya enstrümantasyon testleri olarak çalıştırılabilirler. Örnek: Tek bir parça ve görünüm modeli çiftinin tüm işlevlerini test etme.
  • Uçtan uca testler (E2e): Birlikte çalışan özelliklerin kombinasyonunu test edin. Uygulamanın büyük bir bölümünü test ederler, gerçek kullanımı yakından simüle ederler ve bu nedenle genellikle yavaştırlar. En yüksek doğruluk oranına sahiptirler ve uygulamanızın bir bütün olarak gerçekten çalıştığını gösterirler. Genel olarak bu testler, androidTest kaynak kümesindeki enstrümantasyon testleri olacaktır.
    Örnek: Uygulamanın tamamını başlatıp birkaç özelliği birlikte test etme.

Bu testlerin önerilen oranı genellikle bir piramitle gösterilir. Testlerin büyük çoğunluğu birim testleridir.

Mimari ve Test

Uygulamanızı test piramidinin tüm farklı seviyelerinde test edebilme olanağınız, uygulamanızın mimarisiyle doğrudan bağlantılıdır. Örneğin, son derece kötü tasarlanmış bir uygulama, tüm mantığını tek bir yöntemin içine yerleştirebilir. Bu testler uygulamanın büyük bir bölümünü test etme eğiliminde olduğundan, bunun için uçtan uca test yazabilirsiniz. Ancak birim veya entegrasyon testleri yazmak için ne yapmanız gerekir? Kodun tamamı tek bir yerde olduğundan yalnızca tek bir birim veya özellikle ilgili kodu test etmek zordur.

Daha iyi bir yaklaşım, uygulama mantığını birden fazla yönteme ve sınıfa ayırarak her parçanın ayrı ayrı test edilmesini sağlamaktır. Mimari, kodunuzu bölüp düzenlemenin bir yoludur. Bu sayede birim ve entegrasyon testleri daha kolay yapılabilir. Test edeceğiniz YAPILACAKLAR uygulamasında belirli bir mimari kullanılıyor:



Bu derste, yukarıdaki mimarinin bölümlerini uygun şekilde izole ederek nasıl test edeceğinizi göreceksiniz:

  1. Öncelikle depoyu birim testi yaparsınız.
  2. Ardından, görünüm modelinde bir test çifti kullanırsınız. Bu, görünüm modelinin birim testi ve entegrasyon testi için gereklidir.
  3. Ardından, parçalar ve bunların görünüm modelleri için entegrasyon testleri yazmayı öğreneceksiniz.
  4. Son olarak, Navigation Component'i içeren entegrasyon testleri yazmayı öğreneceksiniz.

Uçtan uca test konusunu bir sonraki derste ele alacağız.

Bir sınıfın bir bölümü (bir yöntem veya küçük bir yöntem koleksiyonu) için birim testi yazdığınızda amacınız yalnızca o sınıftaki kodu test etmektir.

Yalnızca belirli bir sınıftaki veya sınıflardaki kodu test etmek zor olabilir. Bunu bir örnek üzerinde inceleyelim. data.source.DefaultTaskRepository sınıfını main kaynak kümesinde açın. Bu, uygulamanın deposudur ve bir sonraki adımda birim testleri yazacağınız sınıftır.

Amacınız yalnızca söz konusu sınıftaki kodu test etmektir. Ancak DefaultTaskRepository işlevini yerine getirmek için LocalTaskDataSource ve RemoteTaskDataSource gibi diğer sınıflara bağlıdır. Diğer bir deyişle, LocalTaskDataSource ve RemoteTaskDataSource, DefaultTaskRepository'nin bağımlılıklarıdır.

Bu nedenle, DefaultTaskRepository içindeki her yöntem, veri kaynağı sınıflarındaki yöntemleri çağırır. Bu yöntemler de bilgileri bir veritabanına kaydetmek veya ağ ile iletişim kurmak için diğer sınıflardaki yöntemleri çağırır.



Örneğin, DefaultTasksRepo içindeki bu yönteme göz atın.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks, deponuza yapabileceğiniz en "temel" çağrılardan biridir. Bu yöntemde, SQLite veritabanından okuma ve ağ çağrıları (updateTasksFromRemoteDataSource çağrısı) yapma işlemleri yer alır. Bu, yalnızca depo kodundan çok daha fazla kod içerir.

Deponun test edilmesinin zor olmasının daha spesifik nedenlerinden bazıları şunlardır:

  • Bu depo için en basit testleri bile yapmak üzere veritabanı oluşturma ve yönetme konusunda düşünmeniz gerekir. Bu durum, "Bu test yerel mi yoksa enstrümantasyonlu mu olmalı?" ve simüle edilmiş bir Android ortamı elde etmek için AndroidX Test'i kullanıp kullanmamanız gerektiği gibi soruları gündeme getirir.
  • Ağ kodu gibi kodun bazı bölümlerinin çalışması uzun sürebilir veya bazen başarısız olabilir. Bu da uzun süren, güvenilmez testlere yol açar.
  • Testleriniz, test hatasıyla ilgili hangi kodun hatalı olduğunu teşhis etme özelliğini kaybedebilir. Testleriniz, depo dışı kodu test etmeye başlayabilir. Bu nedenle, örneğin, sözde "depo" birim testleriniz, veritabanı kodu gibi bağımlı kodlardan bazılarındaki bir sorun nedeniyle başarısız olabilir.

Test Çiftleri

Bunun çözümü, depoyu test ederken gerçek ağ veya veritabanı kodunu kullanmamak, bunun yerine test çifti kullanmaktır. Test çifti, özellikle test için tasarlanmış bir sınıf sürümüdür. Testlerde sınıfın gerçek sürümünün yerini alması amaçlanmıştır. Bu, dublörlerin tehlikeli sahnelerde gerçek oyuncunun yerine geçmesine benzer.

Bazı test çifti türleri şunlardır:

Sahte

Sınıfın "çalışan" bir uygulamasına sahip olan ancak testler için uygun, üretim için uygun olmayan bir şekilde uygulanan test çifti.

Mock

Hangi yöntemlerinin çağrıldığını izleyen bir test çifti. Daha sonra, yöntemlerinin doğru şekilde çağrılıp çağrılmadığına bağlı olarak testi geçer veya testte başarısız olur.

Stub

Hiçbir mantık içermeyen ve yalnızca döndürmesi için programladığınız şeyi döndüren bir test çifti. Bir StubTaskRepository, örneğin getTasks'deki belirli görev kombinasyonlarını döndürecek şekilde programlanabilir.

Dummy

Yalnızca parametre olarak sağlamanız gerektiği durumlarda olduğu gibi, etrafta dolaşan ancak kullanılmayan bir test çifti. NoOpTaskRepository olsaydı yöntemlerin hiçbirinde kod olmadan yalnızca TaskRepository uygulanırdı.

Spy

Bazı ek bilgileri de takip eden bir test çifti. Örneğin, SpyTaskRepository işlemi yaptıysanız addTask yönteminin kaç kez çağrıldığını takip edebilir.

Test çiftleri hakkında daha fazla bilgi için Testing on the Toilet: Know Your Test Doubles (Tuvalette Test: Test Çiftlerinizi Tanıyın) başlıklı makaleyi inceleyin.

Android'de en sık kullanılan test çiftleri Fakes ve Mocks'tur.

Bu görevde, gerçek veri kaynaklarından DefaultTasksRepository ayrılmış birim testi için FakeDataSource test çifti oluşturacaksınız.

1. adım: FakeDataSource sınıfını oluşturun

Bu adımda, LocalDataSource ve RemoteDataSource öğesinin test çifti olacak FakeDataSouce adlı bir sınıf oluşturacaksınız.

  1. Test kaynak kümesinde, sağ tıklayıp Yeni -> Paket'i seçin.

  1. İçinde kaynak paketi bulunan bir veri paketi oluşturun.
  2. data/source paketinde FakeDataSource adlı yeni bir sınıf oluşturun.

2. adım: TasksDataSource arayüzünü uygulayın

Yeni sınıfınızı FakeDataSource test çifti olarak kullanabilmek için diğer veri kaynaklarının yerine geçebilmesi gerekir. Bu veri kaynakları TasksLocalDataSource ve TasksRemoteDataSource'dır.

  1. Bu ikisinin de TasksDataSource arayüzünü nasıl uyguladığına dikkat edin.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. FakeDataSource, TasksDataSource'i uygulasın:
class FakeDataSource : TasksDataSource {

}

Android Studio, TasksDataSource için gerekli yöntemleri uygulamadığınızdan şikayet edecektir.

  1. Hızlı düzeltme menüsünü kullanın ve Üyeleri uygula'yı seçin.


  1. Tüm yöntemleri seçip Tamam'a basın.

3. adım: getTasks yöntemini FakeDataSource'ta uygulayın

FakeDataSource, sahte adı verilen belirli bir test çifti türüdür. Sahte, sınıfın "çalışan" bir uygulamasına sahip olan ancak testler için uygun, üretim için uygun olmayan bir şekilde uygulanan bir test dublörüdür. "Çalışan" uygulama, sınıfın girişlere göre gerçekçi çıkışlar üreteceği anlamına gelir.

Örneğin, sahte veri kaynağınız ağa bağlanmaz veya bir veritabanına hiçbir şey kaydetmez. Bunun yerine yalnızca bellek içi bir liste kullanır. Bu, görevleri alma veya kaydetme yöntemlerinin beklenen sonuçları döndürmesi açısından "beklediğiniz gibi çalışır" ancak sunucuya veya veritabanına kaydedilmediği için bu uygulamayı üretimde hiçbir zaman kullanamazsınız.

FakeDataSource

  • gerçek bir veritabanına veya ağa güvenmenize gerek kalmadan DefaultTasksRepository içindeki kodu test etmenizi sağlar.
  • testler için "yeterince gerçekçi" bir uygulama sağlar.
  1. FakeDataSource oluşturucusunu, boş değiştirilebilir liste varsayılan değerine sahip bir MutableList<Task>? olan tasks adlı bir var oluşturacak şekilde değiştirin.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Bu, veritabanı veya sunucu yanıtı gibi davranan görevlerin listesidir. Şimdilik hedef, deponun getTasks yöntemini test etmektir. Bu işlem, veri kaynağının getTasks, deleteAllTasks ve saveTask yöntemlerini çağırır.

Bu yöntemlerin sahte bir versiyonunu yaz:

  1. getTasks yazın: tasks, null değilse Success sonucunu döndürün. tasks değeri null ise Error sonucunu döndür.
  2. Yazma deleteAllTasks: Değiştirilebilir görevler listesini temizler.
  3. Yazma saveTask: Görevi listeye ekler.

FakeDataSource için uygulanan bu yöntemler aşağıdaki koda benzer.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

Gerekirse içe aktarma ifadeleri şunlardır:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

Bu, gerçek yerel ve uzak veri kaynaklarının işleyiş şekline benzer.

Bu adımda, az önce oluşturduğunuz sahte test çiftini kullanabilmek için manuel bağımlılık yerleştirme adı verilen bir teknik kullanacaksınız.

Asıl sorun, FakeDataSource olması ancak testlerde nasıl kullandığınızın net olmamasıdır. Yalnızca testlerde TasksRemoteDataSource ve TasksLocalDataSource yerine geçmesi gerekir. Hem TasksRemoteDataSource hem de TasksLocalDataSource, DefaultTasksRepository'nin bağımlılıklarıdır. Bu, DefaultTasksRepositories'nin çalışması için bu sınıfların gerekli olduğu veya bu sınıflara "bağlı" olduğu anlamına gelir.

Şu anda bağımlılıklar, DefaultTasksRepository sınıfının init yöntemi içinde oluşturuluyor.

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

taskLocalDataSource ve tasksRemoteDataSource öğelerini DefaultTasksRepository içinde oluşturup atadığınız için bunlar esasen sabit kodlanmış olur. Test çiftinizi kullanmanın bir yolu yoktur.

Bunun yerine, bu veri kaynaklarını sabit kodlamak yerine sınıfa sağlamanız gerekir. Bağımlılık sağlama işlemine bağımlılık yerleştirme adı verilir. Bağımlılıkları sağlamanın farklı yolları vardır. Bu nedenle, farklı türlerde bağımlılık yerleştirme vardır.

Oluşturucu Bağımlılık Ekleme, oluşturucuya ileterek test çiftini değiştirmenize olanak tanır.

Enjeksiyon yok

Ekleme (Injection)

1. adım: DefaultTasksRepository'de Constructor Dependency Injection'ı kullanın

  1. DefaultTaskRepository oluşturucusunu, Application yerine hem veri kaynaklarını hem de coroutine dağıtıcısını alacak şekilde değiştirin (testleriniz için de değiştirmeniz gerekir. Bu, coroutine'lerle ilgili üçüncü ders bölümünde daha ayrıntılı olarak açıklanmıştır).

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Bağımlılıkları ilettiğiniz için init yöntemini kaldırın. Artık bağımlılık oluşturmanız gerekmez.
  2. Eski örnek değişkenlerini de silin. Bunları oluşturucuda tanımlıyorsanız:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Son olarak, yeni oluşturucuyu kullanmak için getRepository yöntemini güncelleyin:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

Artık oluşturucu bağımlılığı eklemeyi kullanıyorsunuz.

2. adım: Testlerinizde FakeDataSource'u kullanın

Kodunuz artık oluşturucu bağımlılığı eklemeyi kullandığına göre, DefaultTasksRepository öğenizi test etmek için sahte veri kaynağınızı kullanabilirsiniz.

  1. DefaultTasksRepository sınıf adını sağ tıklayın ve Oluştur'u, ardından Test'i seçin.
  2. Test kaynak grubunda DefaultTasksRepositoryTest oluşturmak için talimatları uygulayın.
  3. Yeni DefaultTasksRepositoryTest sınıfınızın en üstüne, sahte veri kaynaklarınızdaki verileri temsil etmek için aşağıdaki üye değişkenlerini ekleyin.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Üç değişken oluşturun: iki FakeDataSource üye değişkeni (deponuzdaki her veri kaynağı için bir tane) ve test edeceğiniz DefaultTasksRepository için bir değişken.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Test edilebilir bir DefaultTasksRepository oluşturup başlatmak için bir yöntem oluşturun. Bu DefaultTasksRepository, test dublörünüz FakeDataSource'ü kullanacak.

  1. createRepository adlı bir yöntem oluşturun ve bunu @Before ile açıklama ekleyin.
  2. remoteTasks ve localTasks listelerini kullanarak sahte veri kaynaklarınızı oluşturun.
  3. Yeni oluşturduğunuz iki sahte veri kaynağını ve Dispatchers.Unconfined kullanarak tasksRepository öğenizi oluşturun.

Nihai yöntem aşağıdaki koda benzemelidir.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

3. adım: DefaultTasksRepository getTasks() testini yazın

DefaultTasksRepositoryTest yazma zamanı!

  1. Deponun getTasks yöntemi için bir test yazın. getTasks işlevini true ile çağırdığınızda (yani uzak veri kaynağından yeniden yüklenmesi gerektiğinde) yerel veri kaynağı yerine uzak veri kaynağındaki verileri döndürdüğünü kontrol edin.

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

getTasks: numarasını aradığınızda hata alırsınız

4. adım: runBlockingTest işlevini ekleyin

getTasks, suspend işlevi olduğundan ve bunu çağırmak için bir ortak yordam başlatmanız gerektiğinden ortak yordam hatası beklenir. Bunun için bir coroutine kapsamına ihtiyacınız vardır. Bu hatayı düzeltmek için testlerinizde başlatma eşzamanlılıklarını işlemek üzere bazı gradle bağımlılıkları eklemeniz gerekir.

  1. testImplementation kullanarak, test kaynak grubuna eşzamanlı rutinleri test etmek için gereken bağımlılıkları ekleyin.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Senkronize etmeyi unutmayın.

kotlinx-coroutines-test, özellikle coroutine'leri test etmek için tasarlanmış coroutine test kitaplığıdır. Testlerinizi çalıştırmak için runBlockingTest işlevini kullanın. Bu, coroutines test kitaplığı tarafından sağlanan bir işlevdir. Bir kod bloğunu alır ve bu kod bloğunu eşzamanlı ve anında çalışan özel bir eş yordam bağlamında çalıştırır. Bu nedenle, işlemler belirlenmiş bir sırada gerçekleşir. Bu, temel olarak eş yordamlarınızın eş yordam olmayanlar gibi çalışmasını sağlar. Bu nedenle, kod testi için tasarlanmıştır.

suspend işlevini çağırırken test sınıflarınızda runBlockingTest kullanın. runBlockingTest işleyiş şekli ve eş yordamların nasıl test edileceği hakkında daha fazla bilgiyi bu serinin bir sonraki codelab'inde edineceksiniz.

  1. Sınıfın üstüne @ExperimentalCoroutinesApi ekleyin. Bu, sınıfta deneysel bir coroutine API'si (runBlockingTest) kullandığınızı bildiğinizi ifade eder. Aksi takdirde uyarı alırsınız.
  2. DefaultTasksRepositoryTest bölümüne geri dönün ve runBlockingTest ekleyerek testinizin tamamını bir kod "bloğu" olarak alın.

Bu son test, aşağıdaki koda benzer.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. Yeni getTasks_requestsAllTasksFromRemoteDataSource testinizi çalıştırın ve hatanın giderildiğini doğrulayın.

Bir depoyu birim testine tabi tutmanın nasıl yapılacağını öğrendiniz. Bu sonraki adımlarda, bağımlılık eklemeyi tekrar kullanacak ve başka bir test çifti oluşturacaksınız. Bu kez, görünüm modelleriniz için birim ve entegrasyon testlerinin nasıl yazılacağını göstereceksiniz.

Birim testleri yalnızca ilgilendiğiniz sınıfı veya yöntemi test etmelidir. Bu, izolasyon içinde test olarak bilinir. Burada "biriminizi" net bir şekilde izole eder ve yalnızca bu birimin parçası olan kodu test edersiniz.

Bu nedenle, TasksViewModelTest yalnızca TasksViewModel kodunu test etmeli, veritabanında, ağda veya depodaki sınıflarda test yapmamalıdır. Bu nedenle, görünüm modelleriniz için de tıpkı deponuzda yaptığınız gibi sahte bir depo oluşturacak ve testlerinizde kullanmak üzere bağımlılık ekleme uygulayacaksınız.

Bu görevde, bağımlılık eklemeyi görünüm modellerine uygulayacaksınız.

1. Adım: TasksRepository arayüzü oluşturma

Oluşturucu bağımlılığı eklemeyi kullanmaya yönelik ilk adım, sahte ve gerçek sınıf arasında paylaşılan ortak bir arayüz oluşturmaktır.

Bu özellik pratikte nasıl görünüyor? TasksRemoteDataSource, TasksLocalDataSource ve FakeDataSource'ye bakın. Hepsinin aynı arayüzü paylaştığını göreceksiniz: TasksDataSource. Bu sayede, DefaultTasksRepository oluşturucusunda TasksDataSource aldığınızı belirtebilirsiniz.

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

Bu sayede FakeDataSource öğenizi değiştirebiliriz.

Ardından, veri kaynakları için yaptığınız gibi DefaultTasksRepository için bir arayüz oluşturun. DefaultTasksRepository'nın tüm herkese açık yöntemlerini (herkese açık API yüzeyi) içermelidir.

  1. DefaultTasksRepository simgesini açın ve sınıf adını sağ tıklayın. Ardından Refactor -> Extract -> Interface'i (Yeniden düzenle -> Çıkar -> Arayüz) seçin.

  1. Ayrı dosyaya çıkar'ı seçin.

  1. Extract Interface (Arayüzü Çıkar) penceresinde arayüz adını TasksRepository olarak değiştirin.
  2. Arayüz oluşturacak üyeler bölümünde, iki tamamlayıcı üye ve özel yöntemler hariç tüm üyeleri işaretleyin.


  1. Yeniden düzenle'yi tıklayın. Yeni TasksRepository arayüzü, data/source paketinde görünmelidir.

Ve DefaultTasksRepository artık TasksRepository'ı uyguluyor.

  1. Her şeyin hâlâ düzgün çalıştığından emin olmak için uygulamanızı (testleri değil) çalıştırın.

2. adım: Create FakeTestRepository

Arayüzü oluşturduğunuza göre artık DefaultTaskRepository test çiftini oluşturabilirsiniz.

  1. Test kaynak kümesinde, data/source içinde Kotlin dosyasını ve sınıfını FakeTestRepository.kt oluşturun ve TasksRepository arayüzünden genişletin.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Arayüz yöntemlerini uygulamanız gerektiği belirtilir.

  1. Öneri menüsünü görene kadar imleci hatanın üzerinde tutun, ardından Üyeleri uygula'yı tıklayıp seçin.
  1. Tüm yöntemleri seçip Tamam'a basın.

3. Adım: FakeTestRepository yöntemlerini uygulama

Artık "uygulanmadı" yöntemleri içeren bir FakeTestRepository sınıfınız var. FakeDataSource'yı uyguladığınız yönteme benzer şekilde, yerel ve uzak veri kaynakları arasında karmaşık bir arabuluculukla uğraşmak yerine FakeTestRepository, bir veri yapısıyla desteklenir.

FakeTestRepository'nızın FakeDataSource veya benzeri bir şey kullanması gerekmediğini unutmayın. Yalnızca girişlere göre gerçekçi sahte çıkışlar döndürmesi gerekir. Görev listesini depolamak için LinkedHashMap, gözlemlenebilir görevleriniz için ise MutableLiveData kullanacaksınız.

  1. FakeTestRepository içinde hem mevcut görev listesini temsil eden bir LinkedHashMap değişkeni hem de gözlemlenebilir görevleriniz için bir MutableLiveData ekleyin.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

Aşağıdaki yöntemleri uygulayın:

  1. getTasks—Bu yöntem, tasksServiceData öğesini alıp tasksServiceData.values.toList() kullanarak liste haline getirmeli ve bunu Success sonucu olarak döndürmelidir.
  2. refreshTasksobservableTasks değerini, getTasks() tarafından döndürülen değerle günceller.
  3. observeTasksrunBlocking kullanarak bir eş yordam oluşturur ve refreshTasks'ı çalıştırır, ardından observableTasks'ı döndürür.

Bu yöntemlerin kodu aşağıda verilmiştir.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

4. Adım. addTasks'e test için bir yöntem ekleyin

Test yaparken, deponuzda önceden bazı Tasks olması daha iyidir. saveTask işlevini birkaç kez çağırabilirsiniz ancak bunu kolaylaştırmak için özellikle görev eklemenize olanak tanıyan testler için bir yardımcı yöntem ekleyin.

  1. Bir vararg görev alan, her birini HashMap'ye ekleyen ve ardından görevleri yenileyen addTasks yöntemini ekleyin.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

Bu noktada, temel yöntemlerden birkaçı uygulanmış şekilde test için sahte bir deponuz var. Ardından, bunu testlerinizde kullanın.

Bu görevde, ViewModel içinde sahte bir sınıf kullanıyorsunuz. TasksRepository değişkenini TasksViewModel oluşturucusuna ekleyerek iki veri kaynağını oluşturucu bağımlılık ekleme yoluyla almak için oluşturucu bağımlılık eklemeyi kullanın.

Bu işlem, doğrudan oluşturmadığınız için görünüm modellerinde biraz farklıdır. Örneğin:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


Yukarıdaki kodda olduğu gibi, görünüm modelini oluşturan viewModel's property delegate'i kullanıyorsunuz. Görünüm modelinin oluşturulma şeklini değiştirmek için ViewModelProvider.Factory ekleyip kullanmanız gerekir. ViewModelProvider.Factory hakkında daha fazla bilgi edinmek için burayı ziyaret edebilirsiniz.

1. Adım: TasksViewModel'de ViewModelFactory oluşturma ve kullanma

Tasks ekranıyla ilgili sınıfları ve testi güncelleyerek işe başlayın.

  1. TasksViewModel tıklayın.
  2. TasksViewModel oluşturucusunu, sınıfın içinde oluşturmak yerine TasksRepository alacak şekilde değiştirin.

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

Oluşturucuyu değiştirdiğiniz için artık TasksViewModel oluşturmak üzere bir fabrika kullanmanız gerekiyor. Fabrika sınıfını TasksViewModel ile aynı dosyaya yerleştirin. Ancak kendi dosyasına da yerleştirebilirsiniz.

  1. TasksViewModel dosyasının en altında, sınıfın dışında, düz bir TasksRepository alan bir TasksViewModelFactory ekleyin.

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


Bu, ViewModel öğelerinin oluşturulma şeklini değiştirmek için kullanılan standart yöntemdir. Artık fabrikanız olduğuna göre, görünüm modelinizi oluşturduğunuz her yerde kullanabilirsiniz.

  1. Fabrikayı kullanmak için TasksFragment uygulamasını güncelleyin.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Uygulama kodunuzu çalıştırın ve her şeyin çalışmaya devam ettiğinden emin olun.

2. adım: TasksViewModelTest içinde FakeTestRepository'yi kullanma

Artık görünüm modeli testlerinizde gerçek depo yerine sahte depoyu kullanabilirsiniz.

  1. TasksViewModelTest.
  2. TasksViewModelTest ürünündeki FakeTestRepository mülkünü ekleyin.

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. Üç görev içeren bir FakeTestRepository oluşturmak için setupViewModel yöntemini güncelleyin ve ardından bu depoyla tasksViewModel'ı oluşturun.

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. Artık AndroidX Test ApplicationProvider.getApplicationContext kodunu kullanmadığınız için @RunWith(AndroidJUnit4::class) ek açıklamasını da kaldırabilirsiniz.
  2. Testlerinizi çalıştırın ve hepsinin hâlâ çalıştığından emin olun.

Oluşturucu bağımlılık eklemeyi kullanarak artık DefaultTasksRepository bağımlılığını kaldırdınız ve testlerde FakeTestRepository ile değiştirdiniz.

3. Adım: Ayrıca TaskDetail Fragment ve ViewModel'i de güncelleyin.

TaskDetailFragment ve TaskDetailViewModel için aynı değişiklikleri yapın. Bu işlem, bir sonraki TaskDetail testlerini yazarken kodu hazırlayacaktır.

  1. TaskDetailViewModel tıklayın.
  2. Oluşturucuyu güncelleyin:

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. TaskDetailViewModel dosyasının en altına, sınıfın dışına bir TaskDetailViewModelFactory ekleyin.

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. Fabrikayı kullanmak için TasksFragment uygulamasını güncelleyin.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Kodunuzu çalıştırın ve her şeyin çalıştığından emin olun.

Artık TasksFragment ve TasksDetailFragment'de gerçek depo yerine FakeTestRepository kullanabilirsiniz.

Ardından, parçacık ve görünüm modeli etkileşimlerinizi test etmek için entegrasyon testleri yazacaksınız. Görünüm modeli kodunuzun kullanıcı arayüzünüzü uygun şekilde güncelleyip güncellemediğini öğreneceksiniz. Bunun için

  • ServiceLocator modeli
  • Espresso ve Mockito kitaplıkları

Entegrasyon testleri , birlikte kullanıldıklarında beklendiği gibi davrandıklarından emin olmak için çeşitli sınıfların etkileşimini test eder. Bu testler yerel olarak (test kaynak grubu) veya enstrümantasyon testleri (androidTest kaynak grubu) olarak çalıştırılabilir.

Bu durumda, her bir parçayı alıp parçanın ana özelliklerini test etmek için parça ve görünüm modeliyle ilgili entegrasyon testleri yazacaksınız.

1. Adım: Gradle bağımlılıkları ekleme

  1. Aşağıdaki Gradle bağımlılıklarını ekleyin.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

Bu bağımlılıklar şunlardır:

  • junit:junit—Temel test ifadeleri yazmak için gerekli olan JUnit.
  • androidx.test:core—Core AndroidX test library
  • kotlinx-coroutines-test—Eş yordam testi kitaplığı
  • androidx.fragment:fragment-testing—Testlerde parça oluşturmak ve durumlarını değiştirmek için AndroidX test kitaplığı.

Bu kitaplıkları androidTest kaynak kümenizde kullanacağınız için bunları bağımlılık olarak eklemek üzere androidTestImplementation kullanın.

2. adım: TaskDetailFragmentTest sınıfı oluşturun.

TaskDetailFragment, tek bir görevle ilgili bilgileri gösterir.

Diğer parçalara kıyasla oldukça temel işlevlere sahip olduğundan TaskDetailFragment için bir parça testi yazarak başlayacaksınız.

  1. taskdetail.TaskDetailFragment tıklayın.
  2. Daha önce yaptığınız gibi TaskDetailFragment için bir test oluşturun. Varsayılan seçimleri kabul edin ve androidTest kaynak kümesine (test kaynak kümesine DEĞİL) yerleştirin.

  1. TaskDetailFragmentTest sınıfına aşağıdaki notları ekleyin.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

Bu ek açıklamaların amacı şudur:

  • @MediumTest: Testi, @SmallTest birim testleri ve @LargeTest büyük uçtan uca testlere kıyasla "orta çalışma süreli" bir entegrasyon testi olarak işaretler. Bu, testleri gruplandırmanıza ve hangi boyutta test yapacağınızı seçmenize yardımcı olur.
  • @RunWith(AndroidJUnit4::class): AndroidX Test'in kullanıldığı tüm sınıflarda kullanılır.

3. Adım: Testten parça başlatma

Bu görevde, AndroidX Testing kitaplığını kullanarak TaskDetailFragment başlatacaksınız. FragmentScenario, bir parçayı sarmalayan ve test için parçanın yaşam döngüsü üzerinde doğrudan kontrol sağlayan bir AndroidX Test sınıfıdır. Parçalar için test yazmak üzere, test ettiğiniz parça için bir FragmentScenario oluşturursunuz (TaskDetailFragment).

  1. Bu testi kopyalayıp TaskDetailFragmentTest içine yapıştırın.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Yukarıdaki kod:

Bu test henüz tamamlanmamıştır, çünkü herhangi bir iddiada bulunmamaktadır. Şimdilik testi çalıştırın ve neler olduğunu gözlemleyin.

  1. Bu, enstrümanlı bir testtir. Bu nedenle, emülatörün veya cihazınızın görünür olduğundan emin olun.
  2. Testi çalıştırın.

Birkaç şey gerçekleşir.

  • Öncelikle, bu bir enstrümanlı test olduğundan test, fiziksel cihazınızda (bağlıysa) veya bir emülatörde çalışır.
  • Parçayı başlatması gerekir.
  • Başka bir parçaya gitmediğine veya etkinlikle ilişkili menüleri olmadığına dikkat edin. Bu, yalnızca parçadır.

Son olarak, yakından bakın ve görev verileri başarıyla yüklenmediği için parçanın "Veri yok" yazdığını fark edin.

Testinizin hem TaskDetailFragment öğesini yüklemesi (bunu yaptınız) hem de verilerin doğru şekilde yüklendiğini onaylaması gerekir. Neden veri yok? Bunun nedeni, bir görev oluşturmuş olmanız ancak bu görevi depoya kaydetmemiş olmanızdır.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

FakeTestRepository öğesine sahipsiniz ancak parçanız için gerçek deponuzu sahte olanla değiştirmenin bir yolunu bulmanız gerekiyor. Ardından, bu adımı uygularsınız.

Bu görevde, ServiceLocator kullanarak sahte deponuzu parçanıza sağlayacaksınız. Bu sayede parçanızı ve görünüm modeli entegrasyon testlerinizi yazabilirsiniz.

Daha önce, görünüm modeline veya depoya bağımlılık sağlamanız gerektiğinde yaptığınız gibi burada oluşturucu bağımlılık eklemeyi kullanamazsınız. Oluşturucu bağımlılığı ekleme, sınıfı oluşturmanızı gerektirir. Parçalar ve etkinlikler, oluşturmadığınız ve genellikle oluşturucusuna erişemediğiniz sınıflara örneklerdir.

Parçayı oluşturmadığınız için, oluşturucu bağımlılığı ekleme yöntemini kullanarak depo test çiftini (FakeTestRepository) parçayla değiştiremezsiniz. Bunun yerine hizmet bulucu modelini kullanın. Hizmet Bulucu kalıbı, Bağımlılık Ekleme'ye alternatif bir yöntemdir. Bu, hem normal hem de test kodu için bağımlılıklar sağlamak amacıyla "Hizmet Bulucu" adlı tekil bir sınıf oluşturmayı içerir. Normal uygulama kodunda (main kaynak grubu), bu bağımlılıkların tümü normal uygulama bağımlılıklarıdır. Testler için, bağımlılıkların test çifti sürümlerini sağlamak üzere hizmet bulucuyu değiştirirsiniz.

Servis Bulucu'yu kullanmıyorsanız


Servis Bulucu'yu kullanma

Bu codelab uygulamasında şunları yapın:

  1. Bir depoyu oluşturup saklayabilen bir hizmet bulma sınıfı oluşturun. Varsayılan olarak "normal" bir depo oluşturur.
  2. Kodunuzu, bir depoya ihtiyacınız olduğunda hizmet bulucuyu kullanacak şekilde yeniden düzenleyin.
  3. Test sınıfınızda, "normal" depoyu test çiftinizle değiştiren Service Locator üzerinde bir yöntem çağırın.

1. Adım: ServiceLocator'ı oluşturun

ServiceLocator sınıfı oluşturalım. Ana uygulama kodu tarafından kullanıldığından, uygulamanın geri kalan kodlarıyla birlikte ana kaynak kümesinde yer alır.

Not: ServiceLocator tekil bir öğe olduğundan sınıf için Kotlin object anahtar kelimesini kullanın.

  1. Ana kaynak kümesinin en üst düzeyinde ServiceLocator.kt dosyasını oluşturun.
  2. ServiceLocator adlı bir object tanımlayın.
  3. database ve repository örnek değişkenleri oluşturun ve her ikisini de null olarak ayarlayın.
  4. Depoyu @Volatile ile açıklama ekleyin. Çünkü bu depo birden fazla iş parçacığı tarafından kullanılabilir (@Volatile ile ilgili ayrıntılı açıklamayı burada bulabilirsiniz).

Kodunuz aşağıdaki gibi görünmelidir.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

Şu anda ServiceLocator'nın yapması gereken tek şey TasksRepository döndürmeyi bilmektir. Önceden var olan bir DefaultTasksRepository döndürülür veya gerekirse yeni bir DefaultTasksRepository oluşturulup döndürülür.

Aşağıdaki işlevleri tanımlayın:

  1. provideTasksRepository: Mevcut bir depoyu sağlar veya yeni bir depo oluşturur. Birden fazla iş parçacığının çalıştığı durumlarda yanlışlıkla iki depo örneği oluşturmamak için bu yöntem synchronized açık this olmalıdır.
  2. createTasksRepository: Yeni bir depo oluşturma kodu. createTaskLocalDataSource işlevini çağırıp yeni bir TasksRemoteDataSource oluşturur.
  3. createTaskLocalDataSource—Yeni bir yerel veri kaynağı oluşturma kodu. createDataBase aranacak.
  4. createDataBase: Yeni bir veritabanı oluşturma kodu.

Tamamlanmış kod aşağıda verilmiştir.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

2. adım: Uygulamada ServiceLocator'ı kullanma

Depoyu tek bir yerde, ServiceLocator konumunda oluşturmak için ana uygulama kodunuzda (testlerinizde değil) bir değişiklik yapacaksınız.

Depo sınıfının yalnızca bir örneğini oluşturmanız önemlidir. Bunu sağlamak için Uygulama sınıfımda hizmet bulucuyu kullanacaksınız.

  1. Paket hiyerarşinizin en üst düzeyinde TodoApplication dosyasını açın, deponuz için bir val oluşturun ve ServiceLocator.provideTaskRepository kullanılarak elde edilen bir depo atayın.

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Uygulamada bir depo oluşturduğunuza göre artık DefaultTasksRepository içindeki eski getRepository yöntemini kaldırabilirsiniz.

  1. DefaultTasksRepository simgesini açın ve tamamlayıcı nesneyi silin.

DefaultTasksRepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

Artık getRepository kullandığınız her yerde uygulamanın taskRepository özelliğini kullanın. Bu sayede, depoyu doğrudan oluşturmak yerine ServiceLocator tarafından sağlanan depoyu alırsınız.

  1. TaskDetailFragement'ı açın ve sınıfın üst kısmında getRepository görüşmesini bulun.
  2. Bu çağrıyı, TodoApplication'dan depoyu alan bir çağrıyla değiştirin.

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. Aynı işlemi TasksFragment için de yapın.

TasksFragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. StatisticsViewModel ve AddEditTaskViewModel için, depoyu alan kodu TodoApplication'deki depoyu kullanacak şekilde güncelleyin.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Uygulamanızı (testi değil) çalıştırın.

Yalnızca yeniden düzenleme yaptığınız için uygulama sorunsuz bir şekilde çalışmaya devam eder.

3. Adım: Create FakeAndroidTestRepository

Test kaynağı grubunda zaten bir FakeTestRepository var. Varsayılan olarak test ve androidTest kaynak kümeleri arasında test sınıflarını paylaşamazsınız. Bu nedenle, androidTest kaynak kümesinde FakeTestRepository sınıfının bir kopyasını oluşturmanız ve bu kopyayı FakeAndroidTestRepository olarak adlandırmanız gerekir.

  1. androidTest kaynak kümesini sağ tıklayın ve bir veri paketi oluşturun. Tekrar sağ tıklayın ve kaynak paketi oluşturun.
  2. Bu kaynak paketinde FakeAndroidTestRepository.kt adlı yeni bir sınıf oluşturun.
  3. Aşağıdaki kodu ilgili sınıfa kopyalayın.

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

4. Adım. ServiceLocator'ınızı testlere hazırlama

Tamam, test yaparken ServiceLocator simgesini kullanarak test çiftlerini değiştirme zamanı. Bunu yapmak için ServiceLocator kodunuza bazı kodlar eklemeniz gerekir.

  1. ServiceLocator.kt tıklayın.
  2. tasksRepository için ayarlayıcıyı @VisibleForTesting olarak işaretleyin. Bu ek açıklama, ayarlayıcının herkese açık olmasının nedeninin test olduğunu ifade etmenin bir yoludur.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Testinizi tek başına veya bir grup testle birlikte çalıştırıyor olmanız fark etmeksizin testleriniz tam olarak aynı şekilde çalışmalıdır. Bu, testlerinizin birbirine bağlı davranışlar içermemesi gerektiği (yani testler arasında nesne paylaşımından kaçınılması) anlamına gelir.

ServiceLocator tekil bir öğe olduğundan testler arasında yanlışlıkla paylaşılabilir. Bunu önlemek için testler arasında ServiceLocator durumunu düzgün şekilde sıfırlayan bir yöntem oluşturun.

  1. Any değeriyle lock adlı bir örnek değişken ekleyin.

ServiceLocator.kt

private val lock = Any()
  1. Veritabanını temizleyen ve hem depoyu hem de veritabanını null olarak ayarlayan, teste özel bir resetRepository yöntemi ekleyin.

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

5. Adım: ServiceLocator'ınızı kullanma

Bu adımda ServiceLocator kullanırsınız.

  1. TaskDetailFragmentTest tıklayın.
  2. lateinit TasksRepository değişkeni bildirin.
  3. Her testten önce FakeAndroidTestRepository oluşturmak ve her testten sonra temizlemek için bir kurulum ve bir yıkım yöntemi ekleyin.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. activeTaskDetails_DisplayedInUi() işlev gövdesini runBlockingTest içine alın.
  2. Parçayı başlatmadan önce activeTask öğesini depoya kaydedin.
repository.saveTask(activeTask)

Son test aşağıdaki kod gibi görünür.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. @ExperimentalCoroutinesApi ile sınıfın tamamına not ekleyin.

İşlem tamamlandığında kod aşağıdaki gibi görünür.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. activeTaskDetails_DisplayedInUi() testini çalıştırın.

Eskisi gibi, bu kez de parçayı görmeniz gerekir. Ancak bu kez, depoyu doğru şekilde ayarladığınız için görev bilgileri gösterilir.


Bu adımda, ilk entegrasyon testinizi tamamlamak için Espresso kullanıcı arayüzü testi kitaplığını kullanacaksınız. Kodunuzu, kullanıcı arayüzünüz için onaylamalar içeren testler ekleyebileceğiniz şekilde yapılandırmış olmanız gerekir. Bunun için Espresso test kitaplığını kullanırsınız.

Espresso'nun yardımıyla:

  • Görünümlerle etkileşimde bulunma (ör. düğmeleri tıklama, çubuğu kaydırma veya ekranda aşağı kaydırma)
  • Belirli görünümlerin ekranda olduğunu veya belirli bir durumda olduğunu (ör. belirli bir metni içerdiğini ya da bir onay kutusunun işaretli olduğunu) onaylayın.

1. Adım: Gradle Bağımlılığına Dikkat Edin

Android projelerinde varsayılan olarak bulunduğundan ana Espresso bağımlılığına zaten sahip olursunuz.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core: Bu temel Espresso bağımlılığı, yeni bir Android projesi oluşturduğunuzda varsayılan olarak eklenir. Çoğu görünüm ve bu görünümlerdeki işlemler için temel test kodunu içerir.

2. adım: Animasyonları devre dışı bırakma

Espresso testleri gerçek bir cihazda çalıştırıldığından doğası gereği araç testleridir. Karşılaşılan sorunlardan biri animasyonlardır: Bir animasyon gecikirse ve bir görünümün ekranda olup olmadığını test etmeye çalışırsanız ancak animasyon devam ediyorsa Espresso testi yanlışlıkla başarısız edebilir. Bu durum, Espresso testlerinin güvenilir olmamasına neden olabilir.

Espresso kullanıcı arayüzü testi için animasyonları devre dışı bırakmanız önerilir (böylece testiniz daha hızlı çalışır):

  1. Test cihazınızda Ayarlar > Geliştirici seçenekleri'ne gidin.
  2. Şu üç ayarı devre dışı bırakın: Pencere animasyonu ölçeği, Geçiş animasyonu ölçeği ve Animatör süre ölçeği.

3. Adım: Espresso testine bakma

Espresso testi yazmadan önce bazı Espresso kodlarına göz atın.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

Bu ifade, task_detail_complete_checkbox kimlikli onay kutusu görünümünü bulur, tıklar ve ardından işaretlendiğini onaylar.

Espresso ifadelerinin çoğu dört bölümden oluşur:

1. Statik Espresso yöntemi

onView

onView, Espresso ifadesini başlatan statik bir Espresso yöntemine örnektir. onView en yaygın olanlardan biridir ancak onData gibi başka seçenekler de vardır.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId, kimliğine göre görüntülenen bir ViewMatcher örneğidir. Dokümanlarda inceleyebileceğiniz başka görünüm eşleştiriciler de vardır.

3. ViewAction

perform(click())

perform yöntemi ViewAction sürer. ViewAction, görünümde yapılabilecek bir işlemdir. Örneğin, burada görünümü tıklama işlemi yapılıyor.

4. ViewAssertion

check(matches(isChecked()))

check Bu işlem ViewAssertion sürer. ViewAssertions check or asserts something about the view. En sık kullanacağınız ViewAssertion, matches onaylamasıdır. Onaylamayı tamamlamak için başka bir ViewMatcher kullanın. Bu durumda isChecked kullanın.

Bir Espresso ifadesinde her zaman hem perform hem de check öğesini çağırmadığınızı unutmayın. check kullanarak yalnızca bir iddiada bulunan ifadeler veya perform kullanarak yalnızca ViewAction işlemi yapabilirsiniz.

  1. TaskDetailFragmentTest.kt tıklayın.
  2. activeTaskDetails_DisplayedInUi testini güncelleyin.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Gerekirse içe aktarma ifadeleri şunlardır:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. // THEN yorumundan sonraki her şey Espresso'yu kullanır. Test yapısını ve withId kullanımını inceleyin ve ayrıntılar sayfasının nasıl görünmesi gerektiğiyle ilgili iddialarda bulunup bulunamayacağınızı kontrol edin.
  2. Testi çalıştırın ve geçtiğini onaylayın.

4. Adım. İsteğe bağlı: Kendi Espresso testinizi yazın

Şimdi kendiniz bir test yazın.

  1. completedTaskDetails_DisplayedInUi adlı yeni bir test oluşturun ve bu iskelet kodunu kopyalayın.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. Önceki teste bakarak bu testi tamamlayın.
  2. Çalıştır'ı tıklayın ve testin başarılı olduğunu onaylayın.

Tamamlanan completedTaskDetails_DisplayedInUi şu koda benzemelidir.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

Bu son adımda, sahte adı verilen farklı bir test çifti türünü ve Mockito adlı test kitaplığını kullanarak Navigation bileşenini nasıl test edeceğinizi öğreneceksiniz.

Bu codelab'de sahte olarak adlandırılan bir test çifti kullandınız. Sahteler, test çiftlerinin birçok türünden biridir. Navigation bileşenini test etmek için hangi test çiftini kullanmalısınız?

Navigasyonun nasıl gerçekleştiğini düşünün. TasksFragment bölümündeki görevlerden birine basarak görev ayrıntıları ekranına gittiğinizi düşünün.

Aşağıda, TasksFragment içinde yer alan ve basıldığında görev ayrıntıları ekranına giden kod verilmiştir.

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


Gezinme, navigate yöntemi çağrıldığı için gerçekleşir. Bir onaylama ifadesi yazmanız gerekiyorsa TaskDetailFragment öğesine gidip gitmediğinizi test etmenin basit bir yolu yoktur. Gezinme, TaskDetailFragment başlatmanın dışında net bir çıkış veya durum değişikliğiyle sonuçlanmayan karmaşık bir eylemdir.

navigate yönteminin doğru işlem parametresiyle çağrıldığını onaylayabilirsiniz. Sahte test çifti tam olarak bunu yapar: Belirli yöntemlerin çağrılıp çağrılmadığını kontrol eder.

Mockito, test çiftleri oluşturmak için kullanılan bir çerçevedir. API'de ve adında "mock" kelimesi kullanılsa da bu, yalnızca sahte veriler oluşturmak için değildir. Ayrıca, sahte ve casus işlevler de oluşturabilir.

Mockito'yu kullanarak, navigate yönteminin doğru şekilde çağrıldığını onaylayabilecek bir sahte NavigationController oluşturacaksınız.

1. Adım: Gradle bağımlılıkları ekleme

  1. Gradle bağımlılıklarını ekleyin.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core: Bu, Mockito bağımlılığıdır.
  • dexmaker-mockito: Bu kitaplık, Android projesinde Mockito'yu kullanmak için gereklidir. Mockito'nun çalışma zamanında sınıflar oluşturması gerekir. Android'de bu işlem dex bayt kodu kullanılarak yapılır. Bu nedenle bu kitaplık, Mockito'nun Android'de çalışma zamanında nesneler oluşturmasını sağlar.
  • androidx.test.espresso:espresso-contrib: Bu kitaplık, DatePicker ve RecyclerView gibi daha gelişmiş görünümler için test kodu içeren harici katkılardan (adının nedeni budur) oluşur. Ayrıca, erişilebilirlik kontrolleri ve daha sonra ele alınacak CountingIdlingResource adlı sınıfı da içerir.

2. adım: Create TasksFragmentTest

  1. TasksFragment adlı kişiyi aç.
  2. TasksFragment sınıf adını sağ tıklayın ve Oluştur, ardından Test'i seçin. androidTest kaynak kümesinde bir test oluşturun.
  3. Bu kodu TasksFragmentTest bölümüne kopyalayın.

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

Bu kod, yazdığınız TaskDetailFragmentTest koduna benziyor. FakeAndroidTestRepository oluşturur ve kaldırır. Görev listesindeki bir görevi tıkladığınızda sizi doğru TaskDetailFragment'ya yönlendirdiğini test etmek için bir gezinme testi ekleyin.

  1. Testi ekleyin clickTask_navigateToDetailFragmentOne.

TasksFragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Sahte oluşturmak için Mockito'nun mock işlevini kullanın.

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

Mockito'da sahte oluşturmak için sahte oluşturmak istediğiniz sınıfı iletin.

Ardından, NavController öğenizi parçayla ilişkilendirmeniz gerekir. onFragment, parçanın kendisinde yöntem çağırmanıza olanak tanır.

  1. Yeni sahte verilerinizi parçanın NavController yapın.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Kodu, RecyclerView içinde "TITLE1" metnini içeren öğeyi tıklayacak şekilde ekleyin.
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions, espresso-contrib kitaplığının bir parçasıdır ve RecyclerView'da Espresso işlemleri yapmanıza olanak tanır.

  1. navigate öğesinin doğru bağımsız değişkenle çağrıldığını doğrulayın.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Mockito'nun verify yöntemi, bu işlemi sahte bir işlem haline getirir. Sahte navController öğesinin belirli bir yöntemi (navigate) bir parametreyle (kimliği "id1" olan actionTasksFragmentToTaskDetailFragment) çağırdığını doğrulayabilirsiniz.

Testin tamamı şu şekilde görünür:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Testinizi çalıştırın.

Özetle, gezinmeyi test etmek için şunları yapabilirsiniz:

  1. NavController sahtesi oluşturmak için Mockito'yu kullanın.
  2. Bu sahte NavController öğesini parçaya ekleyin.
  3. navigate işlevinin doğru işlem ve parametrelerle çağrıldığını doğrulayın.

3. Adım: İsteğe bağlı, write clickAddTaskButton_navigateToAddEditFragment

Kendiniz bir gezinme testi yazıp yazamayacağınızı görmek için bu görevi deneyin.

  1. clickAddTaskButton_navigateToAddEditFragment düğmesini tıkladığınızda AddEditTaskFragment sayfasına yönlendirildiğinizi kontrol eden testi yazın.

Yanıt aşağıda verilmiştir.

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

Başladığınız kod ile son kod arasındaki farkı görmek için burayı tıklayın.

Tamamlanmış codelab'in kodunu indirmek için aşağıdaki git komutunu kullanabilirsiniz:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


Alternatif olarak, depoyu Zip dosyası olarak indirebilir, dosyayı açıp Android Studio'da açabilirsiniz.

Zip dosyasını indir

Bu codelab'de manuel bağımlılık ekleme ve hizmet bulucunun nasıl ayarlanacağı, ayrıca Android Kotlin uygulamalarınızda sahte ve taklitlerin nasıl kullanılacağı ele alındı. Özellikle:

  • Test etmek istediğiniz özellikler ve test stratejiniz, uygulamanızda kullanacağınız test türlerini belirler. Birim testleri odaklanmış ve hızlıdır. Entegrasyon testleri, programınızın bölümleri arasındaki etkileşimi doğrular. Uçtan uca testler özellikleri doğrular, en yüksek doğruluğa sahiptir, genellikle ölçülür ve çalıştırılması daha uzun sürebilir.
  • Uygulamanızın mimarisi, test etmenin ne kadar zor olduğunu etkiler.
  • TDD (Test Odaklı Geliştirme), önce testleri yazdığınız, ardından testleri geçecek özelliği oluşturduğunuz bir stratejidir.
  • Uygulamanızın belirli kısımlarını test için izole etmek üzere test çiftlerini kullanabilirsiniz. Test çifti, özellikle test için tasarlanmış bir sınıf sürümüdür. Örneğin, bir veri tabanından veya internetten veri alıyormuş gibi davranıyorsunuz.
  • Gerçek bir sınıfı (ör. bir depo veya ağ katmanı) test sınıfıyla değiştirmek için bağımlılık ekleme'yi kullanın.
  • Kullanıcı arayüzü bileşenlerini başlatmak için enstrümantasyonlu test (androidTest) kullanın.
  • Örneğin bir parçayı başlatmak için oluşturucu bağımlılığı ekleme özelliğini kullanamadığınızda genellikle bir hizmet bulucu kullanabilirsiniz. Hizmet Bulucu deseni, Bağımlılık Ekleme'ye alternatif bir yöntemdir. Bu, hem normal hem de test kodu için bağımlılıklar sağlamak amacıyla "Hizmet Bulucu" adlı tekil bir sınıf oluşturmayı içerir.

Udacity kursu:

Android geliştirici belgeleri:

Videolar:

Diğer:

Bu kurstaki diğer codelab'lerin bağlantıları için Advanced Android in Kotlin codelab'lerinin açılış sayfasına bakın.