Strategi regression test untuk aplikasi reader yang kaya state harus berfokus pada area yang paling sering memunculkan bug berulang: model state lokal, parser feed, engine sinkronisasi, migrasi data, dan transisi offline-first. Untuk jenis aplikasi ini, masalah umumnya bukan hanya satu fungsi yang salah, tetapi interaksi antar-state yang valid secara lokal namun salah secara sistemik.

Karena itu, pendekatan yang paling efektif biasanya bukan memperbanyak end-to-end test. E2E tetap berguna, tetapi terlalu rapuh jika dijadikan garis pertahanan utama. Yang lebih tahan lama adalah test pyramid yang disesuaikan: banyak unit test pada state transition dan parser, integration test deterministik pada database dan sync engine, lalu UI test secukupnya untuk alur yang benar-benar kritis.

Mengapa aplikasi reader/feed sulit dijaga dari regresi

Aplikasi reader yang sinkron dengan feed atau backend agregasi memiliki karakteristik yang membuat bug mudah berulang:

  • State lokal kompleks: status read/unread, starred, bookmark, article expansion state, last sync token, cache image, posisi scroll, hingga penanda artikel yang sudah diunduh.
  • Sumber data tidak rapi: RSS/Atom/feed turunan sering mengandung tanggal tidak konsisten, GUID berubah, konten HTML cacat, encoding aneh, atau item duplikat.
  • Offline-first: aksi pengguna harus tetap tercatat saat offline lalu di-replay saat online tanpa merusak state.
  • Sinkronisasi dua arah: konflik antara perubahan lokal dan remote sering tidak tampak pada test dangkal.
  • Migrasi data: perubahan schema atau model state lama bisa merusak instalasi pengguna lama meski fresh install terlihat aman.

Jika regresi sering muncul, penyebabnya biasanya bukan kekurangan test secara umum, melainkan cakupan test yang salah sasaran. Banyak tim punya UI test untuk membuka layar dan menekan tombol, tetapi tidak punya test yang memverifikasi apakah merge state unread dan sync token tetap benar setelah retry, timeout, atau migrasi.

Menyusun test pyramid yang cocok untuk aplikasi reader

1. Unit test sebagai lapisan utama

Untuk aplikasi kaya state, sebagian besar logika penting harus bisa diuji tanpa UI, jaringan nyata, atau clock sistem. Unit test ideal untuk:

  • State transition pada artikel, feed, folder, dan akun.
  • Aturan merge antara local state dan remote state.
  • Parser feed dan normalisasi field.
  • Penentuan konflik dan resolusinya.
  • Retry policy, backoff policy, dan deduplication key.
  • Seleksi item yang perlu di-sync atau di-prune.

Target utamanya adalah membuat logika inti menjadi fungsi atau service yang deterministik. Jika sebuah aturan bisnis hanya bisa diuji lewat UI, biasanya desainnya terlalu terikat ke framework atau storage.

2. Integration test untuk batas sistem yang penting

Integration test dipakai saat bug muncul dari interaksi beberapa komponen nyata, misalnya:

  • Parser + persistence layer.
  • Sync engine + local database.
  • Migrasi schema + data lama.
  • Queue operasi offline + conflict resolver.

Di sini, gunakan database nyata dalam mode test jika memungkinkan, tetapi tetap stub jaringan dan waktu agar hasilnya deterministik.

3. UI test secukupnya, bukan sebagai fondasi

UI test tetap dibutuhkan, tetapi pilih alur yang benar-benar mewakili risiko tinggi:

  • Menandai artikel read/unread lalu memastikan perubahan terlihat setelah refresh lokal.
  • Membuka feed besar dan memastikan item tidak hilang saat pagination atau refresh.
  • Mode offline: aksi lokal tercatat dan UI tidak menipu pengguna.

Hindari menutup kekurangan unit/integration test dengan menambah UI test untuk setiap bug. Itu biasanya membuat suite lambat, flaky, dan sulit dipelihara.

Kriteria memilih unit, integration, atau UI test

Gunakan pertanyaan sederhana berikut:

  1. Apakah aturan ini murni transformasi state?
    Jika ya, buat unit test.
  2. Apakah bug muncul hanya saat database, serializer, atau migrasi ikut terlibat?
    Jika ya, buat integration test.
  3. Apakah risiko utama ada pada wiring UI, gesture, atau lifecycle layar?
    Jika ya, buat UI test.

Contoh konkret:

  • Artikel jadi unread lagi setelah sync → biasanya unit test pada merge logic, lalu integration test pada replay operasi sync.
  • Feed tertentu gagal diparse karena HTML rusak → kontrak parser + fixture realistis.
  • Setelah upgrade, sebagian artikel hilang → test migrasi data.
  • Tombol mark all as read tidak memperbarui badge → bisa unit test pada derived state, ditambah 1 UI test pada alur visual.

Desain suite regression test yang tahan lama

Struktur suite yang disarankan

tests/
  unit/
    model/
      article_state_tests
      feed_state_tests
      unread_count_tests
    parser/
      rss_parser_tests
      atom_parser_tests
      date_normalization_tests
    sync/
      merge_logic_tests
      conflict_resolution_tests
      retry_policy_tests

  integration/
    database/
      article_repository_tests
      feed_persistence_tests
    sync/
      sync_engine_replay_tests
      offline_queue_tests
      tombstone_handling_tests
    migration/
      schema_migration_tests
      legacy_data_upgrade_tests

  fixtures/
    feeds/
      valid_rss.xml
      valid_atom.xml
      malformed_html.xml
      duplicate_guid.xml
      missing_pubdate.xml
      namespace_edge_case.xml
    sync/
      initial_local_state.json
      remote_delta_page_1.json
      remote_delta_page_2.json
      conflict_local_wins.json
      conflict_remote_wins.json
    migration/
      db_v1.sqlite
      db_v2_expected.sqlite

  ui/
    critical_flows/
      open_feed_and_refresh_tests
      mark_read_offline_tests
      restore_after_restart_tests

Pemisahan seperti ini membantu dua hal: area logika terlihat jelas, dan saat bug muncul tim lebih cepat tahu lapisan test mana yang perlu ditambah.

Aturan penting: setiap bug signifikan harus melahirkan test regresi

Jika bug production ditemukan, jangan hanya memperbaikinya. Simpan input atau state penyebab bug sebagai fixture, lalu buat test yang gagal sebelum patch dan lulus sesudah patch. Dengan begitu, suite test bertambah berdasarkan sejarah bug nyata, bukan asumsi.

Kontrak parser feed: fondasi penting yang sering diremehkan

Parser feed adalah sumber regresi klasik karena input dunia nyata sangat berantakan. Daripada hanya menguji “feed valid berhasil diparse”, lebih baik definisikan kontrak parser yang eksplisit.

Apa yang perlu masuk ke kontrak parser

  • Item tanpa GUID tetap menghasilkan identifier stabil jika memungkinkan.
  • Tanggal invalid tidak boleh membuat parser crash.
  • Konten HTML yang rusak tetap disanitasi atau disimpan dengan fallback aman.
  • Field opsional yang kosong tidak merusak item lain.
  • Duplikasi item ditangani dengan aturan yang konsisten.
  • Namespace yang tidak dikenal diabaikan tanpa memutus parsing utama.

Contoh bentuk test kontrak

func test_parser_handles_missing_pubdate_without_dropping_item() {
    let xml = loadFixture("missing_pubdate.xml")

    let result = FeedParser().parse(xml)

    XCTAssertEqual(result.items.count, 3)
    XCTAssertEqual(result.items[0].title, "Contoh Artikel")
    XCTAssertNil(result.items[0].publishedAt)
}

Test seperti ini lebih berharga daripada test yang hanya memeriksa parser mengembalikan array tidak kosong. Kontrak harus menjelaskan perilaku saat input buruk, karena justru di sanalah regresi paling sering terjadi.

Gunakan fixture realistis, bukan fixture terlalu bersih

Fixture parser sebaiknya diambil dari kasus nyata yang sudah dianonimkan jika perlu. Kategori fixture yang biasanya berguna:

  • Feed dengan item duplikat tetapi link berbeda tipis.
  • Feed yang mengganti GUID antar-publish.
  • Feed dengan CDATA dan HTML nested yang tidak valid.
  • Feed dengan charset atau entity aneh.
  • Feed kosong namun HTTP berhasil.
  • Feed sangat besar untuk menguji memory pressure ringan dan performa parsing dasar.

Kesalahan umum: membuat fixture terlalu ideal. Hasilnya, parser lulus semua test tetapi gagal pada feed sungguhan yang buruk formatnya.

Deterministic sync test untuk engine sinkronisasi

Engine sinkronisasi sering menjadi sumber regresi paling mahal karena melibatkan waktu, urutan event, koneksi, retry, dan konflik. Kuncinya adalah membuat test deterministik.

Prinsip deterministic sync test

  • Jangan gunakan clock sistem langsung. Injeksi clock palsu agar timestamp dapat dikontrol.
  • Jangan gunakan jaringan nyata. Gunakan fake transport atau recorded response.
  • Kontrol urutan respons. Misalnya halaman delta pertama sukses, kedua gagal, lalu retry berhasil.
  • Representasikan perubahan sebagai log operasi, bukan efek samping yang tersebar.
  • Assert state akhir dan log transisi, bukan hanya jumlah item.

Contoh skenario sync yang wajib diuji

  • Local read state dibuat saat offline, lalu server juga mengubah state item yang sama.
  • Sync terputus di tengah pagination delta dan dilanjutkan tanpa menduplikasi item.
  • Item dihapus remote tetapi masih punya referensi lokal.
  • Retry setelah timeout tidak boleh menggandakan operasi write.
  • Sync token kadaluarsa dan harus fallback ke full refresh tanpa kehilangan state penting pengguna.

Contoh model test berbasis replay

func test_offline_read_action_is_replayed_once_after_reconnect() {
    let clock = TestClock(startingAt: "2026-06-15T10:00:00Z")
    let transport = FakeTransport()
    let db = TestDatabase()
    let sync = SyncEngine(clock: clock, transport: transport, database: db)

    db.seedArticles([
        Article(id: "a1", isRead: false)
    ])

    sync.markReadLocally(articleID: "a1")
    XCTAssertTrue(db.article("a1")!.isRead)
    XCTAssertEqual(db.pendingOperations().count, 1)

    transport.enqueueResponse(.success(body: "{\"ok\":true}"))
    sync.runPendingOperations()

    XCTAssertEqual(db.pendingOperations().count, 0)
    XCTAssertEqual(transport.recordedRequests.count, 1)
    XCTAssertTrue(db.article("a1")!.isRead)
}

Yang diuji di sini bukan sekadar “sync berhasil”, tetapi bahwa operasi offline di-replay sekali saja dan state lokal tetap konsisten.

Verifikasi invariant, bukan hanya snapshot hasil

Pada sync engine, test sebaiknya memeriksa invariant seperti:

  • Unread count tidak negatif.
  • Tidak ada dua artikel dengan identity canonical yang sama.
  • Pending operation yang sukses tidak tersisa di queue.
  • Sync token hanya bergerak maju sesuai aturan engine.
  • Rollback parsial tidak meninggalkan relasi yatim.

Invariant seperti ini lebih tahan terhadap refactor dibanding assert yang terlalu bergantung pada detail implementasi.

Pengujian migrasi data: area yang sering terlupakan

Aplikasi reader biasanya hidup lama di perangkat pengguna. Itu berarti database dan model state akan mengalami evolusi. Jika migrasi tidak diuji, regresi bisa hanya muncul pada pengguna lama, bukan pada instalasi baru.

Apa yang harus diuji dalam migrasi

  • Upgrade dari beberapa versi penting, bukan hanya versi sebelumnya.
  • Data parsial atau korup ringan yang masih mungkin ada di lapangan.
  • Field baru dengan default yang benar.
  • Perubahan identity key, indeks, atau relasi antar-entitas.
  • Preservasi state pengguna: read/unread, starred, folder mapping, dan sync metadata.

Pendekatan praktis

  1. Simpan snapshot database lama sebagai fixture.
  2. Jalankan migrasi dalam test.
  3. Verifikasi schema baru dan isi data penting.
  4. Bandingkan invariant bisnis, bukan hanya jumlah record.
func test_migration_preserves_read_state_and_feed_mapping() {
    let db = loadLegacyDatabaseFixture("db_v1.sqlite")

    migrateDatabase(db)

    XCTAssertEqual(db.schemaVersion, currentSchemaVersion)
    XCTAssertEqual(db.unreadCount(feedID: "feed-1"), 12)
    XCTAssertTrue(db.article("legacy-article-42")!.isStarred)
}

Kesalahan umum adalah hanya memeriksa migrasi selesai tanpa error. Itu belum cukup. Migrasi bisa berhasil secara teknis sambil merusak state bisnis pengguna.

Fixture realistis dan data berbasis insiden nyata

Regression suite paling berguna jika fixture-nya merepresentasikan bug nyata. Sumber fixture yang baik:

  • Feed yang pernah membuat parser crash.
  • Respons sync yang pernah memicu duplikasi item.
  • Database lama dari laporan bug yang sudah dianonimkan.
  • Urutan event dari telemetry atau log internal yang direduksi menjadi kasus minimal.

Sebaiknya beri nama fixture berdasarkan gejala atau invariant yang diuji, bukan sekadar nomor acak. Contoh:

  • duplicate_guid_with_changed_link.xml
  • sync_resume_after_page2_timeout.json
  • migration_v3_missing_folder_fk.sqlite

Nama seperti ini membantu reviewer langsung memahami alasan fixture tersebut ada.

Isolasi flaky test sebelum merusak kepercayaan tim

Flaky test sangat berbahaya dalam regression workflow karena tim mulai mengabaikan kegagalan yang sebenarnya valid. Untuk aplikasi kaya state, sumber flaky test biasanya:

  • Ketergantungan pada waktu nyata.
  • Race condition pada async task.
  • State database atau cache yang bocor antar-test.
  • Urutan network callback yang tidak dikendalikan.
  • UI test yang bergantung pada animasi atau timing.

Cara mengurangi flaky test

  • Gunakan test clock dan scheduler yang bisa dikontrol.
  • Reset storage, cache, dan singleton di setiap test.
  • Hindari sleep tetap; tunggu condition yang eksplisit.
  • Jalankan test async dengan dependency injection pada executor atau queue jika arsitektur memungkinkan.
  • Karantina test yang flaky sambil tetap membuka tiket perbaikan, bukan membiarkannya lama merah-hijau acak.

Jika sebuah test gagal karena timing, itu sering menandakan arsitektur yang belum cukup bisa diobservasi. Menambah delay jarang menyelesaikan akar masalah.

Verification workflow sebelum rilis

Regression test yang baik bukan hanya soal menulis test, tetapi juga bagaimana test dijalankan sebelum rilis. Verification workflow yang praktis untuk aplikasi reader biasanya terdiri dari beberapa lapisan.

1. Pada setiap pull request

  • Jalankan seluruh unit test.
  • Jalankan integration test untuk parser, sync, dan persistence.
  • Jalankan subset UI test kritis saja.
  • Wajib tambah test regresi untuk bug yang diperbaiki di PR tersebut.

2. Pada branch rilis atau nightly build

  • Jalankan seluruh suite termasuk migrasi dan compatibility fixture yang lebih berat.
  • Uji kombinasi skenario offline/online dan replay queue.
  • Jalankan parser terhadap kumpulan feed fixture besar.
  • Periksa snapshot log atau invariant sync jika ada harness khusus.

3. Sebelum distribusi final

  • Uji upgrade dari build lama dengan database lama.
  • Lakukan smoke test manual pada alur yang tidak ekonomis diuji otomatis.
  • Pastikan tidak ada flaky test yang sedang diabaikan tanpa analisis.
  • Verifikasi metrik internal atau logging penting masih tersedia untuk debugging pasca-rilis.

Checklist regression untuk perubahan besar pada state model atau engine sinkronisasi

Jika mengubah state model

  • Apakah semua transisi read/unread/starred/bookmark tetap memiliki unit test?
  • Apakah derived state seperti unread count, badge count, atau filtered view ikut diuji?
  • Apakah identity artikel tetap stabil setelah normalisasi field berubah?
  • Apakah persistence round-trip diuji: save lalu load menghasilkan state yang sama?
  • Apakah migrasi dari data lama menutup field baru dengan default yang benar?
  • Apakah state offline yang tertunda tetap kompatibel dengan model baru?

Jika mengubah engine sinkronisasi

  • Apakah urutan operasi masih deterministik dalam test replay?
  • Apakah retry tidak menggandakan write?
  • Apakah conflict resolution punya test untuk local wins dan remote wins?
  • Apakah sync token, pagination marker, atau cursor diuji pada kasus gagal lanjut?
  • Apakah delete/tombstone handling tetap aman?
  • Apakah full refresh fallback tidak menghapus state pengguna yang hanya lokal?
  • Apakah ada test untuk interupsi di tengah sync lalu resume?

Kesalahan umum saat membangun regression test untuk aplikasi reader

  • Terlalu mengandalkan E2E: suite tampak realistis tetapi lambat dan rapuh.
  • Fixture terlalu bersih: parser dan sync lolos test, gagal di dunia nyata.
  • Tidak menguji migrasi: pengguna lama menerima bug yang tak terlihat di CI biasa.
  • Assert terlalu dangkal: hanya memeriksa jumlah item, bukan invariant state.
  • Tidak menyimpan kasus bug production: tim terus mengulang bug lama dengan bentuk berbeda.
  • Flaky test dibiarkan terlalu lama: kepercayaan terhadap suite menurun.

Penutup

Strategi regression test untuk aplikasi reader yang kaya state sebaiknya dibangun dari logika inti yang deterministik, bukan dari otomatisasi UI yang berlebihan. Fokus utama ada pada kontrak parser, state transition, replay sinkronisasi, migrasi data, dan fixture realistis yang berasal dari bug nyata.

Jika test pyramid disusun dengan benar, setiap bug penting akan berubah menjadi aset jangka panjang: sebuah test yang mencegah regresi serupa kembali muncul. Untuk aplikasi reader/feed dengan banyak state lokal dan perilaku offline-first, itulah bentuk perlindungan yang paling praktis sebelum rilis.