iOS Unit Test

Yusuf Kaya
Doğuş Technology
Published in
7 min readMar 29, 2023

Selamlar, bu yazımda detaylı bir şekilde Swift dili ile iOS uygulama geliştirmede unit test yazımından bahsetmeye çalışacağım. Unit Test’in ne olduğunu, ne gibi faydaları olduğunu ve nasıl yazılacağını inceleyeceğiz.

Unit Test Nedir?

Unit test, yazılım geliştirme sürecindeki bir test yöntemidir. Birim testi anlamına gelir. Bu yöntemde, projenin küçük birimlerinin (fonksiyonlar, eventler vb.) bağımlılıklarından kurtarılarak istenildiği gibi doğru çalışıp çalışmadığı test edilir. Bu sayede, hataların erken tespit edilmesi ve giderilmesi sağlanır. Unit testler, yazılımın kalitesini artırmaya ve güvenilirliğini sağlamaya yardımcı olur.

Unit Test’in Faydaları

  • Hataları önceden tespit eder: Unit testler, ilgili birimin olası hatalarını önceden tespit etmek için kullanılır. Bu sayede, hatalar daha önce fark edilip giderilir ve kod kalitesi artar.
  • Bakım maliyetlerini azaltır: Unit testler, projenin bakım maliyetlerini azaltır. Testler, herhangi bir değişiklik yapıldığında, kodun doğru çalışmaya devam ettiğinden emin olmamızı sağlar. Bu sayede, yazılımda yapılan değişikliklerin diğer bölümlere olası etkilerini önceden tespit edebiliriz.
  • Güvenilirliği artırır: Unit testler, yazılımın güvenilirliğini artırır. Testler, yazılımın doğru çalıştığını doğrular ve bu sayede kullanıcıların yazılıma güvenmesini sağlar.
  • Daha iyi bir kod tasarımı: Unit testleri yazarken, kodun test edilebilir olması için daha iyi bir kod tasarımı yapmak gerekiyor. Bu da daha iyi bir kod kalitesi ve daha iyi bir performans sağlar.

Bağımlılıklardan Kurtulmak

Unit test yazarken yalnızca ilgili birimi test edebilmemiz için mümkün olduğu kadar o birimin bağımlılıklarından kurtulmamız gerekiyor. Örnek vermek gerekirse: Test ettiğimiz bölümde bir servis isteği atıldığını düşünelim. O bölümü düzgünce test edebilmemiz için servisin düzgün bir şekilde çalıştığından emin olmamız gerekecek. İşte bahsettiğimiz bu servis bir bağımlılıktır. Test sırasında ilgili servis isteğinin istediğimiz mock datayı dönmesini sağlarsak bu bağımlılıktan kurtulmuş oluruz. Bağımlılıklardan kurtulmak için iki temel yaklaşım vardır:

Dependency inversion: SOLID prensiplerinden biridir ve programlama kodlarının modüler, esnek ve yeniden kullanılabilir olmasını sağlamak için kullanılır. Bağımlılığı azaltmak için yapıların arasına protocol’lerin(interface) oluşturduğu bir katmanın oluşturulmasıdır. Protocol’ler, bir sınıfın ne yapabileceğini tanımlar. Aynı protocol’ün hem mock hem de gerçek dataları dönecek instance’ı olabilir. İhtiyaç duyulan yerde ikisinden biri kullanılabilir. Protocol’ler, bir sınıfın diğer sınıflarla olan bağını gevşeterek test edilebilirliği artırır.

Dependency injection: Bir sınıfın diğer sınıflara bağımlı olduğu durumlarda, dependency injection kullanarak bu bağımlılıkları dışarıdan enjekte ederek kullanabilirsiniz. Bu sayede, unit testlerde bu bağımlılıkları mock nesneleriyle yer değiştirebilir ve testleri daha izole hale getirebilirsiniz. Bir önceki dependency inversion aşamasında protocol’ünü ve mock’unu oluşturduğunuz yapıları, bu aşamada enjekte edebilirsiniz. Bu linkten daha fazla bilgi edinebilirsiniz.

Adım Adım Unit Test Yazımı

Yapımızı Tanıyalım

Uygulamalı olarak test yazımı adımlarını yazmaya başlamadan önce üzerinde örnek göstereceğimiz yapıyı tanıtmak isterim. Yazımda örnek göstereceğim projede sayfalarda bulunan bütün işlemler View Model’de yapılmaktadır. View Model içindeki bu işlemler eventler üzerinden yönetilmektedir. View’da bu eventler’i trigger ederek istediğimiz işlemleri yapmaktayız. Uygulayacağımız sayfa servisten gelen static contenti ekrana basan basit bir sayfadır. Bu sayfa açıldığında servisten content’i çeken bir event ve hata döndüğünde gerekli hata mesajlarını basan bir event olmak üzere 2 adet event vardır. Bu event yönetimi aşağıdaki gibidir:

  func eventTrigger(_ event: Event) {
switch event {
case .viewAppeared:
self.getStaticPageContent(key: key) { [weak self] content in
self?.content = content
}
case let .error(model):
self.alertModel = model
self.hasAlert = true
}
}

Bu kısım test uygulamalı adımlara geçerken yapıyı biraz tanıtmak amacıyla yazılmıştır.

Test Target’ının Eklenmesi

Test yazımına başlamadan önce Unit Test Target’ının oluşturulması gerekmektedir. Bunun için şu adımlar takip edilmeli:

Project > Targets > Add Target > Unit Testing Bundle

Test Target’ı Ekleme Adımları

Modüler Mimari İçin Gerekli Ayarlar

Modüler mimaride yada bir framework’e test target’ı eklendiğinde ana modüle ve host edilen uygulamaya ulaşılamadığı için bazı hatalar ortaya çıkmaktadır. Bunun için Build Settings altında bazı ayarlar yapmak gerekmektedir. Bu ayarlar şu şekildedir

Test Host: $(BUILT_PRODUCTS_DIR)/YourApp.app/YourApp
Bundle Loader: $(TEST_HOST)
Test Target’ı Build Ayarları

Mock Oluşturulması

Unit testlerde mock, test edilen kodun diğer bağımlılıklarını taklit etmek için kullanılır. Bu sayede, test edilen kodun gerçek bağımlılıklarından bağımsız olarak test edilmesi sağlanır. Mocklar, testlerin daha öngörülebilir ve tekrarlanabilir olmasını sağlar ve hataların daha kolay tespit edilmesine yardımcı olur.

Projemizde aşağıdaki gibi bir servis class’ı vardır:

public final class StaticPageService: StaticPageApi {
private let provider = GraphQLProvider.shared.apolloClient

public func getStaticPage(
_ requestInputDTO: GetStaticPageRequestDTO,
completion: @escaping (Result<StaticPageResponseDTO, ApiError>) -> Void
) {
let query = StaticPageQuery(
key: requestInputDTO.key.resourceApiName
)

self.provider.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { result in
switch result {
case let .failure(error):
let wrappedError = ApiError(error: error)
completion(.failure(wrappedError))
case let .success(response):
if let data = response.data?.staticPage {
let staticPage = StaticPageResponseDTO(
id: data.id,
key: data.key,
title: data.title,
body: data.body
)
completion(.success(staticPage))
}
}
}
}

Bu servisi taklit edebilmemiz için protocol’üne ihtiyacımız var:

public protocol StaticPageApi {
func getStaticPage(
_ requestInputDTO: GetStaticPageRequestDTO,
completion: @escaping (Result<StaticPageResponseDTO, ApiError>) -> Void
)
}

Artık bu servisi taklit ederek mock servisimizi oluşturabiliriz:

public final class MockStaticPageService: StaticPageApi {
var staticPageResult: Result<StaticPageResponseDTO, ApiError>!

public func getStaticPage(
_ requestInputDTO: GetStaticPageRequestDTO,
completion: @escaping (Result<StaticPageResponseDTO, ApiError>) -> Void
) {
completion(self.staticPageResult)
}
}

Oluşturduğumuz mock servisi test class’ımızda enjekte ederek bağımlılığımızdan kurtulmuş olacağız.

Test Class’ı Oluşturmak

Artık testimizin senaryolarını kurup, fonksiyonlarını yazacağımız test class’ını açmanın vakti geldi. Target olarak eklediğimiz unit test bundle ile birlikte ilgili modülümüzün altında test klasörü oluşmuş oldu. Bu klasörün altına yeni bir dosya olarak test class’ımızı oluşturuyoruz.

import XCTest

final class StaticPageTests: XCTestCase {
private var viewModel: StaticPageVM!
private var mockService: MockStaticPageService!

override func setUp() {
super.setUp()
self.mockCoordinator = MockStaticCoordinator()
self.mockService = MockStaticPageService()
self.viewModel = StaticPageVM()
}

override func tearDown() {
super.tearDown()
self.mockCoordinator = nil
self.mockService = nil
self.viewModel = nil
}
}

setUp() ve tearDown() Fonksiyonları

Testlerin başlamadan önce ve bittikten sonra yapılacak işlemleri gerçekleştirmek için XCTest framework’ü içinde bulunan setUp ve tearDown fonksiyonlarını kullanırız.

  • setUp: Bu fonksiyon, testler başlamadan önce gerçekleştirilmesi gereken işlemleri içerir. Örneğin, testlerin kullanacağı nesnelerin oluşturulması veya ayarlanması gibi işlemler burada yapılabilir. Her test fonksiyonundan önce çalışır.
  • tearDown: Bu fonksiyon, testlerin tamamlanmasından sonra gerçekleştirilmesi gereken işlemleri içerir. Örneğin, testlerin kullandığı kaynakların temizlenmesi veya nesnelerin silinmesi gibi işlemler burada yapılabilir. Memory leak oluşmaması adına önemlidir. Her test fonksiyonundan sonra çalışır.

@testable import

Bir modülde, fonksiyonların, sınıfların, yapıların erişimimizi etkileyen protection levelları (public, internal, private vb.) vardır. Ancak, @testable import ifadesi kullanılarak, test kodu içinde bu protection levellar aşılarak, test edilen modülün içinde yer alan kodlar test kodu tarafından kullanılabilir hale getirilir. Böylece, test kodu içinde modül içindeki kodlara erişilerek, kodların doğruluğu ve işlevselliği test edilebilir hale gelir. Şunun gibi:

@testable import MyModule
@testable import MyModule2
import XCTest

final class StaticPageTests: XCTestCase {
.
.
.
}

Test Fonksiyonunun Oluşturulması

Artık test fonksiyonunu oluşturabiliriz. Ancak bundan önce senaryonun içeriğiyle ilgili öğrenmemiz gereken bilgiler var. Bunlar test fonksiyonunu nasıl isimlendireceğimiz, Given-When-Then yapısı ve XCTAssert fonksiyonlarıdır.

Test Fonksiyonunun İsimlendirilmesi

İsimlendirme için mevcutta tercih edilen birçok stil var. Benim tercih ettiğim stil de şu şekildedir:

Sonucu etkileyen bir state varsa: test_when(event)_state_should(behavior)

Sonucu etkileyen bir state yoksa: test_when(event)_should(behavior)

Örneğin:

func test_whenViewAppeared_shouldLoadContent()
func test_whenViewAppeared_withFailedService_shouldShowAlert()

Böylelikle senaryomuz çok net anlaşılmış oluyor. Sadece test için değil akışlarımızın da anlaşılması için döküman niteliği taşıyabiliyor.

Given-When-Then

Unit testte Given-When-Then yapısı, test senaryosunu adımlara ayırarak, testin okunabilirliğini arttıran bir yöntemdir. Bu yapının temelinde, test senaryosunu üç bölüme ayırmak yatar:

  • Given (Verilen): Bu bölümde, testin gereksinim duyduğu öncül koşullar belirtilir. Örneğin, bir fonksiyonun test edilmesi durumunda, fonksiyona verilecek parametreler veya önceden oluşturulmuş nesneler gibi koşullar burada belirtilir.
  • When (Eylem): Bu bölümde, testin gerçekleştirileceği eylem belirtilir. Örneğin, bir fonksiyonun çağrılması veya bir nesnenin bir metodu çağrılması gibi testin gerçekleştirileceği eylemler burada belirtilir.
  • Then (Çıktı): Bu bölümde, testin sonucunun ne olması gerektiği belirtilir. Örneğin, bir fonksiyonun çağrılması sonucunda beklenen değer veya bir nesnenin metodu çağrıldığında yapması gereken işlem gibi beklenen sonuçlar burada belirtilir.

XCTAssert Fonksiyonları

XCTest framework’ü içinde bulunan XCTAssert fonksiyonları, test sınıflarında kullanılan ve bir testin başarılı olup olmadığını belirlemeye yardımcı olan fonksiyonlardır. Aşağıda en sık kullanılan assert fonksiyonlarını inceleyelim:

  • XCTAssertEqual: İki değerin eşit olup olmadığını kontrol eder. Değerler eşit değilse, test başarısız olur.
  • XCTAssertNotEqual: İki değerin eşit olmadığını kontrol eder. Değerler eşitse, test başarısız olur.
  • XCTAssertTrue: Bir ifadenin doğru olup olmadığını kontrol eder. İfade yanlışsa, test başarısız olur.
  • XCTAssertFalse: Bir ifadenin yanlış olup olmadığını kontrol eder. İfade doğruysa, test başarısız olur.
  • XCTAssertNil: Bir değerin nil olup olmadığını kontrol eder. Değer nil değilse, test başarısız olur.
  • XCTAssertNotNil: Bir değerin nil olmadığını kontrol eder. Değer nil ise, test başarısız olur.

Bu assert fonksiyonlarından daha fazlası da mevcuttur ve bunların hangisinin kullanılacağı, testin gereksinimlerine ve durumuna bağlıdır.

Test Fonksiyonu ve Test İşlemi

Bütün bu bilgiler ışığında aşağıda bir test senaryosunu inceleyelim:

  func test_whenViewAppeared_shouldLoadContent() {
// Given
self.mockService.staticPageResult = .success(StaticPageResponseDTO.mock)
InjectedValues[\.staticPageService] = mockService

// When
self.viewModel.eventTrigger(.viewAppeared)

// Then
XCTAssertNotNil(self.viewModel.content)
}

Artık oluşturduğumuz senaryoyu test etmek için XCode’da fonksiyonumuzun sol tarafında bulunan test ikonuna tıklamak yeterli olacaktır.

Test Fonksiyonunun Çalıştırılması

Code Coverage

Code coverage, bir yazılımın test edilme derecesini ölçen bir metriktir. Bu metrik, bir uygulamanın kodunun hangi kısmının test edildiğini ve hangi kısmının test edilmediğini gösterir. Oransal olarak ne kadar yüksekse, güvenilirlik o kadar artar. Genel kanı olarak bir uygulamanın güvenilir olabilmesi için minimum 80–90% bandında coverage değerine sahip olması gerekmektedir. Tabi ki bu beklenti değişiklik gösterebilir.

XCode üzerinden de bu oranları görme imkanımız bulunuyor. Bunun için şu adımları takip etmek gerekmektedir:

Test Scheme > Edit Scheme… > Test > Options > Code Coverage

Coverage İçin Scheme Ayarları

Bu işlemden sonra XCode’da sol tarafta bulunan reports altında coverage değerlerini görebiliyor olacaksınız.

Reports Altındaki Test Build’i

Yazımızda Swift dilinde ve iOS platformunda Unit Test konusunu detaylı olarak inceledik. Yararlı olacağını umarak, okuduğunuz için teşekkür ederim.

--

--