CoreData ile Swift’de Yapılacaklar Listesi Oluşturma

Yukarıdaki hata düzeltme testleri son derece faydalı olsa da, daha önce neden olmuş bir hataya tepki olarak yazılırlar ve bizi aynı gerilemeye karşı korurlar.
Yukarıdaki hata düzeltme testleri son derece faydalı olsa da, daha önce neden olmuş bir hataya tepki olarak yazılırlar ve bizi aynı gerilemeye karşı korurlar.

Birim testin ikili test olarak kullanılıp kullanılmayacağına karar vermek bazen kolay olabilir: ya testler tamamen benimsendi ve projemizin tüm kodları test edilebilir olarak tamamlandı – ya da hiç test yapılmayacak.

İkili bir kararla karşı karşıya kaldıklarında, çoğu takımın iki seçeneğin sonunu seçmesi şaşırtıcı değildir – genellikle zaman kısıtlamaları, sıkı son teslim tarihleri ​​ve nakliye gerektiren önemli yeni özellikler nedeniyle.

Bununla birlikte, birim testini bu kadar büyük, ikili bir karar olarak değerlendirmek için hiçbir neden yoktur. Ne de olsa, herhangi bir otomatik test yöntemi, kodumuzu ve onunla çalışma biçimimizi geliştirmek için kullanabileceğimiz başka bir araçtır . Öyleyse bu hafta, ünite testlerini daha pratik bir şekilde uygulamak için birkaç farklı yola göz atalım – acil sorunları çözmek için test kullanmak ve kodumuzu temelden değiştirmek zorunda kalmadan test edilmesini sağlamak.

Hata düzeltmelerini doğrulama

Bir projeye birim testleri eklemeye başlarken – veya mevcut, kısmi bir test takımının kapsamını genişletmek istediğinizde – nereden başlayacağınıza karar vermek genellikle zor olabilir. Kullanıcıya ve uygulamamızın kullanıcı arayüzüne en yakın olan işlevselliği doğrulayarak mı başlıyoruz, sonra da yığından aşağıya doğru mı gidiyoruz yoksa tam tersi mi?

Mükemmel bir başlangıç ​​noktası aramak zorunda kalmak yerine, bir kod tabanında birim test değişikliklerini alışkanlık haline getirmenin harika bir yolu, hata düzeltmelerini doğrulamak için testleri kullanarak başlamaktır. Bu sadece düzeltmeyi hedeflediğimiz hatayı gerçekten düzelttiğimizi doğrulamakla kalmaz , aynı zamanda yavaş yavaş ama kesin bir test paketi oluşturmamıza izin verir – hepsi aynı zamanda kodumuzun kalitesini de arttırır.

Örnek olarak, kullanıcılarımızın çeşitli yazılı içerik biçimlerini okumalarını sağlayan bir uygulama üzerinde çalıştığımızı varsayalım. Tarihsel olarak, uygulamamız yalnızca kitap ve dergileri destekledi, ancak son zamanlarda gazetelere destek de ekledik – bu üç içerik türü bu enum kullanılarak gösteriliyor:

extension Item {
    enum Kind {
        case book
        case magazine
        case newspaper
    }
}

Diyelim ki, gazete özelliğini gönderirken, kullanıcılardan, uygulamanın “Önerilen” bölümünde hiç bir gazetenin görünmediğini belirten raporlar almaya başladık – bu, aşağıdaki kodun neden olduğu bir hata gibi görünüyor:

struct RecommendedItems {
    var books = [Item]()
    var magazines = [Item]()
    var newspapers = [Item]()

    func items(ofKind kind: Item.Kind) -> [Item] {
        if kind == .book {
            return books
        } else {
            return magazines
        }
    }
}

Sorun şu ki, eğer talep edilmezse , o zaman dergilerin geri Item.Kindgönderilmesi .bookgerektiği anlamına gelmeli – bu kodun yazıldığı zaman tam anlam ifade etmiş olabilir (o zamandan beri kitaplar ve dergiler vardı). sadece iki çeşit destekledik.

Elbette yukarıdaki hatayı düzeltip ilerleyebilsek de, bunu gelecekte işlerin devam etmesini sağlayacak bir test eklemek için bir fırsat olarak kullanalım. Bu yüzden düzeltmemizi uygulamadan önce , bunun için bir test ekleyelim – şunun gibi:

class RecommendedItemsTests: XCTestCase {
    func testRecommendedNewspapers() {
        let item = Item(
            kind: .newspaper,
            id: "nytimes",
            name: "The New York Times"
        )

        let recommended = RecommendedItems(newspapers: [item])

        XCTAssertEqual(
            recommended.items(ofKind: .newspaper),
            [item]
        )
    }
}

Yukarıdaki test şu anda başarısız olacaktır, ki bu, kullanıcılarımızın karşılaştığı sorunu yeniden ürettiğinden harikadır. Bu başarısızlık testi uygulandıktan sonra, şimdi tüm durumları ele almak için orijinal if/elseifadelerimizi bir duruma çevirerek hatayı switchdüzeltelim: (işlenmemiş bir dava ile tekrar sonuçlanırsak bize derleme zamanı hatası da verir):

struct RecommendedItems {
    var books = [Item]()
    var magazines = [Item]()
    var newspapers = [Item]()

    func items(ofKind kind: Item.Kind) -> [Item] {
        switch kind {
        case .book: return books
        case .magazine: return magazines
        case .newspaper: return newspapers
        }
    }
}

Yukarıdaki düzeltmeyi uyguladıktan sonra testimiz başarılı olacak – ve düzeltme ekimizi göndermeye hazırız! Yukarıdaki yaklaşım sadece hataları düzeltmek için bize daha fazla güven vermekle kalmaz, aynı hatanın iki kez gerçekleşmeyeceğinden emin olmamızı sağlar, çünkü bir daha aynı regresyona neden olursak – testimiz şimdi söyleyecektir. Biz bu konuda.

Gelecekteki hatalara karşı korunma

Yukarıdaki hata düzeltme testleri son derece faydalı olsa da, daha önce neden olmuş bir hataya tepki olarak yazılırlar ve bizi aynı gerilemeye karşı korurlar. Öyleyse bakalım, bir adım daha ileri gidebilir miyiz, zaten kod tabanının bu bölümünde çalışırken, kendimizle ilgili gerilemelere karşı daha güçlü bir koruma sağlamak için.

Bunu yapmak için, türümüzün her zaman herhangi bir öğeyi geri getirebileceğinden emin RecommendedItemsolacak bir test ekleyelim . Enumumuzu aşağıdakilere uyarak başlayacağız – bu da durumlarını yinelememize izin verecek: Item.KindItem.KindCaseIterable

extension Item.Kind: CaseIterable {}

Daha sonra, Itembirim test hedefimize aşağıdaki uzantıyı ekleyerek, sabitlenmiş değerler (yalnızca test nedenleriyle tamamen yarattığımız örnekler) oluşturmayı biraz daha kolaylaştıralım :

extension Item {
    static func stub(ofKind kind: Kind, id: Item.ID) -> Item {
        return Item(kind: kind, id: id, name: "\(kind)-\(id)")
    }
}

Bu iki küçük uzantı ile, artık Item.Kindher bir tür için tanımlanmış bir öğenin tanımlandığından emin olmak için tüm vakaları yineleyecek olan testimizi yazmaya hazırız – şunun gibi:

class RecommendedItems: XCTestCase {
    ...
    
    func testItemsForAllKinds() {
        let book = Item.stub(ofKind: .book, id: "book")
        let magazine = Item.stub(ofKind: .magazine, id: "magazine")
        let newspaper = Item.stub(ofKind: .newspaper, id: "newspaper")

        let recommended = RecommendedItems(
            books: [book],
            magazines: [magazine],
            newspapers: [newspaper]
        )

        for kind in Item.Kind.allCases {
            let items = recommended.items(ofKind: kind)

            switch kind {
            case .book:
                XCTAssertEqual(items, [book])
            case .magazine:
                XCTAssertEqual(items, [magazine])
            case .newspaper:
                XCTAssertEqual(items, [newspaper])
            }
        }
    }
}

Yukarıdaki test basit olabilir, ancak uygulamalarımızda önerilen öğeleri takip etme biçimimizin, kodumuzu yinelediğimiz için çalışmaya devam edeceğinin – yeni bir içerik türü için yine destek ekleyebilsek bile – daha güçlü bir garanti veriyor. gelecekte.

Pragmatik yeniden düzenleme

Yukarıdaki türden senkron model kod için testler yazmak oldukça kolay olsa da, söz konusu kod akılda tutularak test edilmemişse, asenkron kod alanına girdiğimizde işler gittikçe zorlaşmakta ve zorlaşmaktadır.

Örneğin UserDefaults, kullanıcı onboarding işlemini başarıyla tamamladığında , bir Boole bayrağının bizim uygulamalarımızda doğru bir şekilde saklandığını doğrulamak için bir test yazmak istediğimizi varsayalım. Bu kodun tümü şu anda a’nın içinde yaşıyor OnboardingViewControllerve şöyle görünüyor:

class OnboardingViewController: UIViewController {
    ...

    func finishOnboarding() {
        let task = URLSession.shared.dataTask(
            with: .onboardingFinishedEndpoint
        ) { [weak self] _, _, error in
            DispatchQueue.main.async {
                guard let self = self else { return }

                if let error = error {
                    ErrorPresenter.shared.present(error, in: self)
                    return
                }

                UserDefaults.standard.setValue(true,
                    forKey: UserDefaultsKeys.onboardingFinished
                )

                self.dismiss(animated: true)
            }
        }

        task.resume()
    }
}

Şu anda olduğu gibi, yukarıdaki kod için test yazmak oldukça zor olacaktır. Asıl sorun singletons erişmek için kullanılan olmasıdır URLSessionErrorPresenterve UserDefaultsbize öngörülebilir şekilde İşlevselliğimi doğrulamak için, testlerimizde içinde bu örnekleri üzerinde kontrol altına almak için çok zor (hatta imkansız) yapacak. İdeal olarak, bu bağımlılıkların enjekte edilmesini istiyoruz, böylece onlarla alay edebildik .

Bununla birlikte, OnboardingViewControllerbağımlılık enjeksiyonunu tam olarak desteklemek (ve ayrıca bağımlılıklarımızı tamamen alay edebilecek ölçüde soyutlamak) için tamamen yeniden düzenleme yapmak büyük olasılıkla büyük bir görev olacaktır – bu nedenle, işlevselliğimizi test etmemizi sağlayacak bir yol bulabilecek miyiz bakalım. sadece küçük bir tweaks seti ile.

Bunu yapmanın bir yolu, görünüm denetleyicimizin bağımlılıklarını işlevler kullanarak kapsüllemek ve ardından tüm bu işlevleri içeren bir yapı tanımlamaktır – şunun gibi:

struct OnboardingDependencies {
    // Our networking code, modeled as a function that takes
    // a URL and a completion handler, and then calls the
    // underlying URLSession:
    var networking: (URL, @escaping (Error?) -> Void) -> Void = {
        url, handler in

        let task = URLSession.shared.dataTask(with: url) {
            _, _, error in
            
            DispatchQueue.main.async {
                handler(error)
            }
        }

        task.resume()
    }

    // Our error presenting function can be directly referenced,
    // since Swift supports first class functions: 
    var errorPresenting = ErrorPresenter.shared.present

    // Our key/value persistence code, which we turn into
    // a function that wraps our app's standard UserDefaults:
    var keyValuePesistance: (String, Bool) -> Void = {
        UserDefaults.standard.setValue($1, forKey: $0)
    }
}

Yukarıdakileri dependenciesuyguladıktan sonra, görünüm denetleyicimizde yeni bir özellik tanımlayabilir ve bağımlılıklarımızın işlevselliğine erişmek için yukarıdaki işlevleri çağırabiliriz:

class OnboardingViewController: UIViewController {
    var dependencies = OnboardingDependencies()
    
    ...

    func finishOnboarding() {
        dependencies.networking(.onboardingFinishedEndpoint) {
            [weak self] error in

            guard let self = self else { return }

            if let error = error {
                self.dependencies.errorPresenting(error, self)
                return
            }

            self.dependencies.keyValuePesistance(
                UserDefaultsKeys.onboardingFinished,
                true
            )

            self.dismiss(animated: true)
        }
    }
}

Yukarıdakiler bağımlılık enjeksiyonunu yapmak için ideal yolumuz olmayabilir, ancak oldukça iyi çalışıyor ve görüş denetleyicimizin API’sinde veya bağımlılıklarında herhangi bir değişiklik gerektirmiyor. Herhangi bir yeni protokol tanımlamamız veya kodumuzu temel olarak değiştirmemize gerek yok, ancak yukarıdaki fonksiyonun tamamen test edilmesini sağladık.

Bunu yapmak için basitçe bir örnek oluşturacağız OnboardingViewControllerve daha sonra kontrol altına almak istediğimiz bağımlılık fonksiyonlarını geçersiz kılacağız. Başlangıçta belirlediğimiz testi yazmak için bunu nasıl yapabiliriz – bu, kullanıcı başlama işlemini tamamladığında doğru bayrağın sürdüğünü doğrular:

class OnboardingViewControllerTests: XCTestCase {
    func testPersistingOnboardingFinished() {
        let vc = OnboardingViewController()
        var persistance: (key: String, value: Bool)?

        // Hard-wire our networking function to always
        // return a nil error when called:
        vc.dependencies.networking = { _, handler in
            handler(nil)
        }

        // Override our view controller's key/value persistance
        // function in order to capture its input:
        vc.dependencies.keyValuePesistance = {
            persistance = ($0, $1)
        }

        vc.finishOnboarding()

        // Verify that the correct key and value were persisted:
        XCTAssertEqual(persistance?.key, UserDefaultsKeys.onboardingFinished)
        XCTAssertEqual(persistance?.value, true)
    }
}

Oldukça havalı! Birincil hedefimiz OnboardingViewController, ek bir bonus olarak test edilebilir hale getirilmiş olabilirken , şimdi de bu sınıfa hem daha basit hem de daha esnek hale getirdik – çünkü artık bağımlılıkları ile güçlü bir bağdaştırması bulunmuyor.

Sonuç

Ünite testi başlangıçta çok büyük bir mesele gibi görünse de – tek bir testin yazılmasından önce aylarca yeniden yapılanma çalışması gerektiren bir şey – nadiren böyle olur. Test etmek için kod tabanımızın bazı bölümlerinde bazı değişiklikler yapmamız gerekebilirken , bu değişiklikler genellikle tam olarak geriye dönük uyumlu bir şekilde ve sadece kısa bir süre alabilecek şekilde yapılabilir.

Günün sonunda, ünite testi bir yaşam biçimi değildir, manüel test etme ihtiyacını tamamen ortadan kaldırmaz ya da sihirli bir şekilde hatasız bir uygulama yapmaz – başka herhangi bir araç gibi. Eğer taktiksel ve düşünceli bir şekilde konuşlandırılmışsa, hata düzeltmelerini doğrulamamıza, gelecekteki birçok türde hataları önlememize ve kodumuzun yeni özellikler ve yetenekler eklediğimiz şekilde çalışmamıza devam etmesini sağlamaya yardımcı olan bir araç.

Comments

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

GIPHY App Key not set. Please check settings

Yükleniyor…

0

Ne düşünüyorsun?

Google Play Store’da bir “Web Uygulama” yayınlayın

SimpleDateFormat, DateFormat java sınıfını genişleten bir java sınıfıdır. Yerelleştirme duyarlı bir biçimde normalleştirme, biçimlendirme ve ayrıştırma tarihi için kullanılır.

Android’de DateTime