Kami menulis banyak pengujian unit, mengembangkan aplikasi
SoundCloud untuk iOS. Tes unit terlihat sangat cantik. Mereka pendek, (mudah-mudahan) dapat dibaca, dan mereka memberi kita keyakinan bahwa kode yang kita tulis berfungsi seperti yang diharapkan. Tetapi tes unit, seperti namanya, mencakup hanya satu blok kode, paling sering fungsi atau kelas. Jadi, bagaimana Anda menangkap kesalahan yang ada dalam interaksi antar kelas - kesalahan seperti
kebocoran memori ?
Kebocoran Memori
Terkadang cukup sulit untuk mendeteksi kesalahan kebocoran memori. Ada kemungkinan referensi kuat untuk delegasi, tetapi ada juga kesalahan yang jauh lebih sulit untuk dideteksi. Misalnya, apakah jelas bahwa kode berikut ini mungkin berisi kebocoran memori?
final class UseCase { weak var delegate: UseCaseDelegate? private let service: Service init(service: Service) { self.service = service } func run() { service.makeRequest(handleResponse) } private func handleResponse(response: ServiceResponse) {
Karena
Layanan sudah dilaksanakan, tidak ada jaminan mengenai perilakunya. Melewati fungsi
handleResponse ke fungsi pribadi, yang menangkap
sendiri , kami memberikan
Layanan dengan referensi yang kuat untuk
UseCase . Jika
Layanan memutuskan untuk menyimpan tautan ini - dan kami tidak memiliki jaminan bahwa ini tidak akan terjadi - maka terjadi kebocoran memori. Tetapi dengan studi sepintas kode, tidak jelas bahwa ini benar-benar bisa terjadi.
Ada juga
posting indah
oleh John Sandell tentang menggunakan unit test untuk mendeteksi kebocoran memori untuk kelas. Tetapi dengan contoh di atas, di mana sangat mudah untuk melewatkan kebocoran memori, tidak selalu jelas bagaimana menulis unit test seperti itu. (Tentu saja, kami tidak berbicara di sini dalam hal pengalaman.)
Seperti yang ditulis Guilherme dalam sebuah
posting baru-baru ini, fitur-fitur baru dalam aplikasi SoundCloud untuk iOS ditulis menurut "pola arsitektur bersih" - paling sering ini adalah jenis
VIPER . Sebagian besar modul
VIPER ini dibangun menggunakan apa yang kita sebut
ModuleFactory .
ModuleFactory semacam itu membutuhkan input, dependensi, dan konfigurasi - dan membuat
UIViewController yang sudah terhubung ke modul lainnya dan dapat
didorong ke tumpukan navigasi.
Modul
VIPER ini dapat memiliki beberapa delegasi, pengamat, dan kesalahan pelarian, yang masing-masing dapat menyebabkan pengontrol tetap berada di memori setelah dihapus dari tumpukan navigasi. Ketika ini terjadi, jumlah memori akan meningkat, dan sistem operasi mungkin memutuskan untuk menghentikan aplikasi.
Jadi mungkinkah menutup kemungkinan banyak kebocoran dengan menulis sesedikit mungkin unit test? Jika tidak, maka semua ini adalah buang-buang waktu.
Tes integrasi
Jawabannya, seperti yang sudah Anda tebak dari judul posting ini, adalah ya. Dan kami melakukan ini melalui pengujian integrasi. Tujuan dari tes integrasi adalah untuk menguji bagaimana benda berinteraksi satu sama lain. Tentu saja, modul
VIPER adalah kelompok objek, kebocoran memori adalah salah satu bentuk interaksi yang pasti ingin kita hindari.
Rencana kami sederhana: Kami akan menggunakan
ModuleFactory kami untuk instantiate modul
VIPER . Kemudian kita akan menghapus tautan ke
UIViewController dan memastikan bahwa semua bagian penting dari modul dihancurkan bersama dengan itu.
Masalah pertama yang kita hadapi adalah bahwa pada dasarnya kita tidak dapat dengan mudah mengakses bagian apa pun dari modul
VIPER selain
UIViewController . Satu-satunya fungsi
publik di
ModuleFactory kami adalah
func make () -> UIViewController . Tetapi bagaimana jika kita menambahkan titik masuk lain hanya untuk pengujian kita? Metode baru ini akan dideklarasikan melalui
internal , jadi kita hanya dapat mengaksesnya melalui
impor @testable , framework
ModuleFactory . Ini akan mengembalikan tautan ke semua bagian terpenting dari modul, yang kemudian dapat kami tahan untuk tautan yang lemah untuk masuk dalam pengujian kami. Ini akhirnya terlihat seperti ini:
public final class ModuleFactory {
Ini memecahkan masalah kurangnya akses langsung ke data objek. Jelas, ini tidak ideal, tetapi memenuhi kebutuhan kita, jadi mari kita lanjutkan menulis tes. Ini akan terlihat seperti ini:
final class ModuleMemoryLeakTests: XCTestCase {
Jadi, kami memiliki cara mudah untuk mendeteksi kebocoran memori dalam modul
VIPER . Ini sama sekali tidak ideal dan membutuhkan pekerjaan pengguna tertentu untuk setiap modul baru yang ingin kami uji, tetapi ini tentu saja jauh lebih sedikit daripada menulis unit test terpisah untuk setiap kemungkinan kebocoran memori. Ini juga membantu mengidentifikasi kebocoran memori yang bahkan tidak kami duga. Faktanya, setelah menulis beberapa tes ini, terungkap bahwa kami memiliki tes yang tidak lulus, dan setelah beberapa penelitian, kami menemukan kebocoran memori dalam modul. Setelah koreksi, tes harus diulang.
Ini juga memberi kita titik awal untuk menulis serangkaian tes integrasi yang lebih umum untuk modul. Pada akhirnya, jika kita hanya menyimpan tautan yang kuat ke
Presenter dan mengganti
UIViewController dengan
tiruan , kita dapat memalsukan input pengguna, lalu memanggil metode presenter dan memeriksa tampilan dummy data dalam
View .