logo
hero image

Testable Code

Menulis kode yang mudah untuk di-test pada Swift.
1 May 2023 ยท 8 Minutes

Terkadang kita sebagai Developer tidak menulis Unit Test bukan karena tidak mau, tapi mungkin karena codebase yang ada itu cukup sulit untuk di-test, apalagi jika sudah cukup panjang kode programnya, pasti akan banyak memakan waktu untuk melakukan refactoring agar bisa di-test.

Nah, itulah pentingnya untuk mempertimbangkan Testing dari awal, yang saya maksud bukan hanya mencentang "Include Tests" di Xcode pada saat kita membuat project baru :v namun kita juga harus mendesain kode kita dari awal agar mudah untuk di-test.

Karena mindset-nya sedikit berbeda, kalau kita tidak mempertimbangkan Testing dari awal mungkin kita akan pakai Singleton di mana-mana, melakukan hard-code terhadap dependensi (bukan melalui Dependency Injection), dll. sehingga kode kita akan sulit untuk di-test.

Pada artikel ini saya akan membagikan beberapa tips dalam menulis kode yang testable atau mudah untuk di-test pada bahasa pemrograman Swift.

Unit Test tidak ada gunanya?

Unit Test?Unit Test?

Beberapa orang mungkin menganggap bahwa Unit Test itu tidak ada gunanya, hanya buang-buang waktu, kan lebih baik waktunya digunakan untuk mendevelop fitur?

Padahal sebaliknya, Unit Test justru bisa saving our time! Misalnya jika kita ingin melakukan perubahan pada kode kita seperti menambahkan fitur baru, melakukan refactoring, dll. kita tidak perlu testing secara manual yang akan memakan banyak waktu hanya untuk memastikan aplikasi kita tetap berjalan dengan baik.

Unit Test juga bisa berfungsi sebagai dokumentasi dari program yang kita buat, karena jika kita melihat dari sisi kode kan hanya ada tipe datanya aja ya? nah dengan Unit Test, kita bisa melihat input dan output secara konkrit pada sebuah function.

Dan dengan Unit test, kita bisa lebih percaya diri dengan kode yang kita tulis, karena jika kita melakukan perubahan yang ternyata menyebabkan bug pada aplikasi kita, maka bug tersebut akan terdeteksi lebih awal, jadi nggak akan sampai ke Team QA atau bahkan ke User.

Tips menulis Testable Code

Berikut adalah beberapa tips dalam menulis kode yang mudah untuk di-test pada bahasa pemrograman Swift.

1. Memecah Function

Jika kita mempunyai function yang terlalu panjang, mungkin bisa kita pecah menjadi beberapa function. karena Unit Test kan tujuannya untuk melakukan testing terhadap suatu unit kode, jadi kalau satu function memiliki banyak responsibility, maka akan sulit untuk di-test.

Contohnya seperti ini:

โŒ Contoh yang salah
icon
1struct ProductService {
2 func getProducts() async throws -> [Product] {
3 let url = URL(string: "https://website.com/api/products")!
4 let (data, _) = try await URLSession.shared.data(from: url)
5 return try JSONDecoder().decode([Product].self, from: data)
6 }
7}
โœ… Contoh yang benar
icon
1struct ProductService {
2 func getProducts() async throws -> [Product] {
3 let data = try await fetchProducts()
4 return try decodeProducts(data)
5 }
6
7 internal func fetchProducts() async throws -> Data {
8 let url = URL(string: "https://website.com/api/products")!
9 let (data, _) = try await URLSession.shared.data(from: url)
10 return data
11 }
12
13 internal func decodeProducts(_ data: Data) throws -> [Product] {
14 return try JSONDecoder().decode([Product].self, from: data)
15 }
16}

Pada contoh di atas, kita pecah function getProducts menjadi 2 function, yaitu:

  1. function fetchProducts untuk melakukan Network Request.

  2. function decodeProducts untuk Parsing JSON.

Sehingga kita bisa test secara terisolasi terhadap masing-masing function tersebut.

๐Ÿ’Ž Unit Tests
icon
1import XCTest
2@testable import SampleApp
3
4final class ProductServiceTests: XCTestCase {
5 func test_fetchProducts() async throws { ... }
6
7 func test_decodeProducts() throws {
8 let json = """
9 [
10 {
11 "name": "iPhone 15",
12 "price": 20000000
13 }
14 ]
15 """
16
17 let data = json.data(using: .utf8)!
18 let sut = ProductService()
19 do {
20 let products = try sut.decodeProducts(data)
21 XCTAssertEqual(
22 products,
23 [Product(name: "iPhone 15", price: 20_000_000)]
24 )
25 } catch {
26 XCTFail(error.localizedDescription)
27 }
28 }
29}

2. Hindari Singleton, gunakan Dependency Injection!

Sebisa mungkin kita harus menghindari Singleton, agar lebih mudah jika kita ingin melakukan mocking atau me-replace suatu dependensi pada saat testing.

Solusinya kita bisa gunakan Dependency Injection.

Dependency Injection adalah teknik untuk memindahkan proses pembuatan instance suatu dependensi dari dalam class atau struct menuju ke luar. Jadi class atau struct tersebut hanya menerima instance, bukan yang membuat instance dari suatu dependensi.

โŒ Contoh yang salah
icon
1struct ProductService {
2 func getProducts() async throws -> [Product] {
3 let data = try await fetchProducts()
4 return try decodeProducts(data)
5 }
6
7 internal func fetchProducts() async throws -> Data {
8 let url = URL(string: "https://website.com/api/products")!
9 let (data, _) = try await URLSession.shared.data(from: url)
10 return data
11 }
12
13 internal func decodeProducts(_ data: Data) throws -> [Product] {
14 return try JSONDecoder().decode([Product].self, from: data)
15 }
16}
โœ… Contoh yang benar
icon
1struct ProductService {
2 private let urlSession: URLSession
3 private let jsonDecoder: JSONDecoder
4
5 init(urlSession: URLSession = URLSession.shared, jsonDecoder: JSONDecoder = JSONDecoder()) {
6 self.urlSession = urlSession
7 self.jsonDecoder = jsonDecoder
8 }
9
10 func getProducts() async throws -> [Product] {
11 let data = try await fetchProducts()
12 return try decodeProducts(data)
13 }
14
15 internal func fetchProducts() async throws -> Data {
16 let url = URL(string: "https://website.com/api/products")!
17 let (data, _) = try await urlSession.data(from: url)
18 return data
19 }
20
21 internal func decodeProducts(_ data: Data) throws -> [Product] {
22 return try jsonDecoder.decode([Product].self, from: data)
23 }
24}

Agar lebih mudah dalam menggunakan ProductService, kita bisa tambahkan Default Parameter pada Initializer-nya.

Default Parameter
icon
1struct ProductService {
2 private let urlSession: URLSession
3 private let jsonDecoder: JSONDecoder
4
6 init(urlSession: URLSession = URLSession.shared, jsonDecoder: JSONDecoder = JSONDecoder()) {
7 self.urlSession = urlSession
8 self.jsonDecoder = jsonDecoder
9 }
10
11 ...

Jadi, apabila kita ingin mocking response pada urlSession, maka akan menjadi lebih mudah, contohnya seperti ini:

๐Ÿ’Ž Unit Tests
icon
1import XCTest
2@testable import SampleApp
3
4final class ProductServiceTests: XCTestCase {
5 func test_fetchProducts_failed() async throws {
6 MockURLProtocol.requestHandler = { request in
8 let response = HTTPURLResponse(url: URL(string: "https://mock.com")!, statusCode: 500, httpVersion: nil, headerFields: nil)!
9 return (response, nil)
10 }
11
12 let configuration = URLSessionConfiguration.default
13 configuration.protocolClasses = [MockURLProtocol.self]
14 let urlSession = URLSession.init(configuration: configuration)
15
16 let sut = ProductService(urlSession: urlSession)
17 do {
18 let _ = try await sut.getProducts()
19 XCTFail()
20 } catch {
21 XCTAssert(true)
22 }
23 }
24}

3. Dependency Inversion

Dependency Inversion adalah satu dari 5 prinsip dalam SOLID.

Sederhananya, jika kita punya Object A yang mempunyai dependensi ke Object B, maka Object A tidak boleh punya dependensi langsung ke Object B, tetapi harus melalui abstraction. di Swift, kita bisa menggunakan protocol untuk membuat abstraction tersebut.

โŒ Contoh yang salah
icon
1class ProductViewModel {
2 private let productService: ProductService
3
4 init(productService: ProductService) {
5 self.productService = productService
6 }
7
8 func getTotalProducts() async throws -> Int {
9 let products = try await productService.getProducts()
10 return products.count
11 }
12}
โœ… Contoh yang benar
icon
1protocol ProductServiceProtocol {
2 func getProducts() async throws -> [Product]
3}
4
5class ProductViewModel {
6 private let productService: ProductServiceProtocol
7
8 init(productService: ProductServiceProtocol) {
9 self.productService = productService
10 }
11
12 func getTotalProducts() async throws -> Int {
13 let products = try await productService.getProducts()
14 return products.count
15 }
16}

Pada contoh di atas, ProductViewModel tidak mempunyai dependensi langsung ke ProductService, namun melalui sebuah abstraction bernama ProductServiceProtocol.

Jadi, kita akan lebih mudah untuk melakukan mocking pada saat Testing nantinya.

๐Ÿ’Ž Unit Tests
icon
1import XCTest
2@testable import SampleApp
3
5struct MockProductService: ProductServiceProtocol {
6 func getProducts() async throws -> [Product] {
7 return [Product(name: "iPhone 15", price: 20_000_000)]
8 }
9}
11
12final class ProductViewModelTests: XCTestCase {
13 func test_getTotalProducts() async throws {
14 let sut = ProductViewModel(
16 productService: MockProductService()
17 )
18
19 do {
20 let total = try await sut.getTotalProducts()
21 XCTAssertEqual(total, 1)
22 } catch {
23 XCTFail(error.localizedDescription)
24 }
25 }
26}

4. Error Handling

Kita bisa membuat Custom Error agar errornya lebih jelas dan terdefinisi dengan baik sehingga kita bisa lebih spesifik dalam pengecekan tipe error pada saat menulis Unit Test.

โŒ Contoh yang salah
icon
1internal func fetchProducts() async throws -> Data {
2 let (data, _) = try await urlSession.data(from: url)
3 return data
4}
โœ… Contoh yang benar
icon
1enum AppError: Error {
2 case invalidURL
3 case failedToFetchProducts
4 case failedToDecodeProducts
5}
6
7...
8
9internal func fetchProducts() async throws -> Data {
10 do {
11 guard let url = URL(string: "https://website.com/api/products") else {
12 throw AppError.invalidURL
13 }
14
15 let (data, _) = try await urlSession.data(from: url)
16 return data
17 } catch {
18 throw AppError.failedToFetchProducts
19 }
20}
๐Ÿ’Ž Unit Tests
icon
1func test_fetchProducts_failed() async throws {
2 MockURLProtocol.requestHandler = { request in
3 let response = HTTPURLResponse(url: URL(string: "https://mock.com")!, statusCode: 500, httpVersion: nil, headerFields: nil)!
4 return (response, nil)
5 }
6
7 let configuration = URLSessionConfiguration.default
8 configuration.protocolClasses = [MockURLProtocol.self]
9 let urlSession = URLSession.init(configuration: configuration)
10
11 let sut = ProductService(urlSession: urlSession)
12 do {
13 let _ = try await sut.getProducts()
14 XCTFail()
15 } catch {
17 XCTAssertEqual(error as? AppError, AppError.failedToFetchProducts)
18 }
19}

5. Accessibility Identifier

Yang terakhir, kita bisa menambahkan Accessibility Identifier pada suatu view agar lebih ringkas dan lebih konsisten pada saat melakukan query untuk UI Test.

Misalnya kita punya UILabel yang berisi text deskripsi yang sangat panjang, nah dengan menambahkan Accessbility Identifier, kita hanya perlu query berdasarkan ID nya saja dan jika kita ingin mengubah text nya, kita tidak perlu mengubah query kita.

โŒ Contoh yang salah
icon
1let descriptionLabel: UILabel = {
2 let label = UILabel()
3 label.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
4 label.numberOfLines = 0
5 return label
6}()
โœ… Contoh yang benar
icon
1let descriptionLabel: UILabel = {
2 let label = UILabel()
3 label.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
4 label.numberOfLines = 0
6 label.accessibilityIdentifier = "description"
7 return label
8}()
๐Ÿ’Ž UI Tests
icon
1import XCTest
2
3final class SampleAppUITests: XCTestCase {
4 func test_descriptionLabel_exists() throws {
5 let app = XCUIApplication()
6 app.launch()
7
9 let descriptionLabel = app.staticTexts["description"]
10 XCTAssertTrue(descriptionLabel.exists)
11 }
12}

Itulah tadi beberapa tips dalam menulis kode yang mudah untuk di-test pada Swift. Meskipun saat ini kita belum ingin menambahkan Unit Testing pada aplikasi kita karena alasan tertentu, menurutku kita tetap harus mendesain kode kita agar mudah untuk di-test. Sehingga jika kita ingin menambahkan Unit Test di masa depan nanti, maka akan menjadi lebih mudah dan tidak perlu banyak refactor.

Oke mungkin itu saja yang bisa saya bagikan kali ini, kalau kamu merasa artikel ini bermanfaat silakan Like & Share artikel ini ke teman-teman kamu atau jika kamu punya pertanyaan, tulis aja di kolom komentar, Thank you! ๐Ÿ˜ ๐Ÿ™


Referensi:

iOS Development
Swift
Testing

Written by :
Alfin Syahruddin
Developer ยท Stock Trader ยท Libertarian ยท Freethinker

Always open to new ideas. ๐Ÿ•Š๏ธ

Loading...

Related articles

Articles that you might want to read.

hero image
CI/CD aplikasi iOS dengan Xcode Cloud

Otomatisasi build, testing, dan deployment aplikasi iOS ke Testflight & AppStore.

27 January 2024 ยท 8 Minutes
iOS Development
CI/CD
Xcode Cloud
hero image
CI/CD aplikasi iOS dengan Fastlane dan Github Actions

Otomatisasi build, testing, screenshot, dan deployment aplikasi iOS ke Testflight & AppStore.

24 September 2023 ยท 9 Minutes
iOS Development
CI/CD
Fastlane
Github Actions
hero image
Kustomisasi tema pada Material UI ๐ŸŽจ

Cheatsheet untuk menerapkan Design System pada Material UI

9 April 2021 ยท 9 Minutes
React
Design System
Material UI
Web Development
All rights reserved ยฉ Alfin Syahruddin ยท 2019
RSS Feed