Membangun test suite CatatanKu: unit test untuk logika murni, integration test untuk backend & IPC, dan E2E UI testing dengan Playwright yang menjalankan aplikasi nyata seperti pengguna

CatatanKu sekarang penuh fitur — dan penuh risiko. Setiap perubahan di storage, IPC, atau UI bisa merusak sesuatu yang tampak tidak berhubungan. Testing adalah jaring pengaman yang membuat kalian berani refactor dan rilis tanpa menunggu laporan bug dari pengguna.
Episode ini membangun piramida testing CatatanKu: unit test di dasar (cepat, banyak), integration test di tengah (backend + IPC), dan E2E UI test di puncak (lambat, sedikit, paling dekat dengan pengalaman nyata). Kita pakai Playwright untuk E2E — tool yang sama untuk web dan Electron/Tauri, sehingga skillnya reusable.
Prinsip piramida: makin rendah lapisan, makin cepat dan makin banyak. Jangan membaliknya — suite E2E raksasa akan lambat, rapuh, dan mahal. Kebanyakan bug ditemukan di unit dan integration test.
| Lapisan | Contoh | Kecepatan | Biaya |
|---|---|---|---|
| Unit | reducer, validasi, format | ms | rendah |
| Integration | DB + IPC + service | detik | sedang |
| E2E | klik UI nyata | menit | tinggi |
Unit test terbaik memverifikasi logika murni — fungsi tanpa I/O: reducer (episode 3), validasi (episode 14), format tanggal (episode 11). Contoh testing reducer CatatanKu:
import { describe, expect, it } from "vitest";
import { appReducer, initialState } from "./app-reducer";
describe("appReducer", () => {
it("menyimpan catatan baru tanpa mengubah state lain", () => {
const state = appReducer(initialState, {
type: "note/save",
note: { id: "1", title: "Belajar testing", content: "..." },
});
expect(state.notes).toHaveLength(1);
expect(state.searchQuery).toBe(initialState.searchQuery);
});
it("update catatan mengganti entri yang sama, bukan menduplikasi", () => {
const saved = appReducer(initialState, {
type: "note/save",
note: { id: "1", title: "A", content: "" },
});
const updated = appReducer(saved, {
type: "note/save",
note: { id: "1", title: "A (edit)", content: "" },
});
expect(updated.notes).toHaveLength(1);
expect(updated.notes[0].title).toBe("A (edit)");
});
});Reducers yang murni di episode 3 membayar dividen di sini: test tanpa setup database, tanpa mock framework — murni input/output. Test seperti ini adalah mayoritas suite dan berjalan dalam milidetik.
Integration test memverifikasi bagian yang berinteraksi dengan dunia nyata: database, command IPC, service sinkronisasi. Contoh testing command create_note Tauri dengan database in-memory:
use catatanku_lib::{create_note, list_notes};
#[test]
fn create_and_list_notes() {
// pakai SQLite in-memory untuk test (bukan file produksi)
let db = test_db();
let id = create_note(&db, "Catatan pertama", "isi").unwrap();
let notes = list_notes(&db).unwrap();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].id, id);
assert_eq!(notes[0].title, "Catatan pertama");
}
#[test]
fn rejects_empty_title() {
let db = test_db();
assert!(create_note(&db, " ", "isi").is_err());
}Kunci desain yang memungkinkan ini: backend logic dipisahkan dari framework. Jangan menaruh semua logika di dalam closure command Tauri — taruh di modul library (catatanku_lib) yang bisa diuji tanpa meluncurkan aplikasi. Pattern yang sama di Electron: service logic di module murni, ipcMain.handle hanya menjadi pembungkus tipis.
E2E menjalankan aplikasi lengkap dan berinteraksi seperti pengguna: klik, ketik, tunggu hasil. Playwright mendukung Electron secara native dan Tauri lewat eksekusi binary:
import { test, expect, _electron as electron } from "@playwright/test";
test("pengguna bisa menambah dan mencari catatan", async () => {
const app = await electron.launch({ args: ["dist/main.js"] });
const window = await app.firstWindow();
await window.getByLabel("Judul catatan").fill("Belajar Playwright");
await window.getByRole("button", { name: "Tambah" }).click();
await expect(window.getByText("Belajar Playwright")).toBeVisible();
await window.getByLabel("Cari catatan").fill("playwright");
await expect(window.getByText("Belajar Playwright")).toBeVisible();
await window.getByLabel("Cari catatan").fill("tidak-ada");
await expect(window.getByText("Belajar Playwright")).toBeHidden();
await app.close();
});Untuk Tauri, jalankan binary hasil build dan targetkan window-nya:
import { test, expect } from "@playwright/test";
import { spawn } from "node:child_process";
test("app Tauri terbuka dan merender daftar catatan", async () => {
const app = spawn("./src-tauri/target/release/catatanku");
const win = await connectToWebview(); // helper: temukan webview lewat CDP
await expect(win.getByText("CatatanKu")).toBeVisible();
await win.getByLabel("Cari catatan").fill("rust");
app.kill();
});E2E adalah test perilaku pengguna: fokus pada label yang terlihat (bukan selector internal), sehingga kalau markup berubah, test tetap valid selama perilakunya sama.
Tip
Satu suite E2E yang menutup alur kritis (buka → tambah → cari → ekspor → update) jauh lebih berharga daripada dua puluh test yang menguji selector internal. Pilih alur yang paling sering dipakai pengguna dan buat mereka bisa dijalankan di CI (episode 17) tanpa kepala yang berkedip.
| Lapisan | Tauri | Electron | Flutter |
|---|---|---|---|
| Unit | cargo test | Vitest/Jest | flutter test |
| Integration | cargo test + in-memory DB | Vitest + real DB | flutter test + sqflite_common_ffi |
| E2E | Playwright + binary | Playwright (Electron) | integration_test + flutter drive |
app-reducer.test.ts berdampingan app-reducer.ts.Inti yang harus dibawa pulang:
Di episode 16 selanjutnya kita membuat CatatanKu benar-benar cepat: performance desktop — pengelolaan memori, startup time, dan CPU profiling dengan pendekatan ukur-dulu-baru-optimasi. Sampai jumpa di episode 16!