Belajar Neovim - Testing & Code Running dengan Neotest
Episode 26 of 28

Belajar Neovim - Testing & Code Running dengan Neotest

Sebuah IDE tanpa integrasi testing hanyalah editor teks yang cantik. Episode ini membahas `neotest` — cara menjalankan dan menavigasi test langsung dari Neovim dengan adapter untuk Go, Python, dan JavaScript/TypeScript, sehingga test-suite jadi bagian dari alur kerja harian, bukan ritual terpisah.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Pendahuluan

Setelah di episode 25 sebelumnya kita membahas perbandingan antara config kustom dan distro Neovim — dan bagaimana menjadikan Neovim sebagai daily driver — pada episode kali ini kita akan mengisi salah satu celah yang selama ini sering kita abaikan: testing. Bayangkan kalian sedang memperbaiki bug di kode produksi. Alur kerja di IDE GUI biasanya: edit file → tekan kombinasi tombol → test berjalan → hasil muncul di panel → kalian lompat ke baris yang gagal → perbaiki. Sekarang bayangkan alur yang sama di Neovim — itulah yang akan kita bangun hari ini.

Neotest adalah framework testing untuk Neovim yang mengubah editor kalian menjadi test runner interaktif. Ia menemukan test-test di dalam kode kalian (berkat Treesitter), menjalankannya secara asinkron, menampilkan status di gutter, dan membuka output — semua tanpa meninggalkan editor. Bukan sekadar "menjalankan perintah di terminal" — ia memahami struktur test kalian seperti IDE sungguhan.

Mengapa ini penting di dunia kerja nyata? Test adalah jaring pengaman utama seorang engineer. Semakin cepat dan semakin mudah menjalankan test, semakin sering kita menjalankannya — dan semakin cepat kita menemukan bug, semakin murah biayanya. Engineer yang mengintegrasikan testing ke editor-nya menulis kode yang lebih berani refactor dan lebih jarang menembak kaki sendiri. Episode ini akan membahas setup neotest, adapter untuk Go, Python, dan JavaScript/TypeScript, serta alur kerja lengkap dari menjalankan satu test hingga seluruh suite.

Pembahasan Utama

Apa Itu Neotest dan Bagaimana Ia Bekerja

Neotest bekerja dengan arsitektur inti (core) + adapter. Inti-nya menyediakan UI, manajemen proses, dan hasil. Adapter adalah "jembatan" yang tahu cara menemukan dan menjalankan test untuk bahasa tertentu — misalnya adapter Go tahu bahwa test ada di file _test.go, adapter Python tahu konvensi pytest, dan seterusnya.

Alur kerjanya kira-kira seperti ini:

  1. Kalian berdiri di atas sebuah test (atau di dalam file test) dan menekan keymap run.
  2. Neotest menggunakan Treesitter untuk menemukan posisi test terdekat di buffer.
  3. Adapter menerjemahkan posisi itu menjadi perintah test yang tepat (go test -run TestFoo, pytest test_foo.py, npm test -- --runInBand).
  4. Proses berjalan asinkron — kalian bisa terus mengedit sambil menunggu.
  5. Hasilnya ditampilkan di gutter (ikon pass/fail), summary window, dan output panel.

Ini pola yang sama dengan extension test runner di IDE modern — tapi berjalan di dalam Neovim yang sudah kalian kenal.

Setup Dasar Neotest

Seperti semua plugin di seri ini, kita pasang melalui lazy.nvim. Neotest butuh beberapa dependency: plenary.nvim (utility), nvim-nio (async library), dan nvim-treesitter (untuk discovery posisi test).

lua/plugins/neotest.lua — setup dasar
{
  "nvim-neotest/neotest",
  event = "VeryLazy",
  dependencies = {
    "nvim-lua/plenary.nvim",
    "nvim-neotest/nvim-nio",
    "nvim-treesitter/nvim-treesitter",
  },
  keys = {
    { "<leader>tt", function() require("neotest").run.run(vim.fn.expand("%")) end, desc = "Run File (Neotest)" },
    { "<leader>tT", function() require("neotest").run.run(vim.uv.cwd()) end, desc = "Run All Tests (Neotest)" },
    { "<leader>tr", function() require("neotest").run.run() end, desc = "Run Nearest (Neotest)" },
    { "<leader>tl", function() require("neotest").run.run_last() end, desc = "Run Last (Neotest)" },
    { "<leader>ts", function() require("neotest").summary.toggle() end, desc = "Toggle Summary (Neotest)" },
    { "<leader>to", function() require("neotest").output.open({ enter = true, auto_close = true }) end, desc = "Show Output (Neotest)" },
    { "<leader>tO", function() require("neotest").output_panel.toggle() end, desc = "Toggle Output Panel (Neotest)" },
    { "<leader>tS", function() require("neotest").run.stop() end, desc = "Stop (Neotest)" },
    { "<leader>tw", function() require("neotest").watch.toggle(vim.fn.expand("%")) end, desc = "Toggle Watch (Neotest)" },
    { "]n", function() require("neotest").jump.next({ status = "failed" }) end, desc = "Next Failed Test" },
    { "[n", function() require("neotest").jump.prev({ status = "failed" }) end, desc = "Prev Failed Test" },
  },
  config = function()
    require("neotest").setup({
      adapters = {},
    })
  end,
}

Important

Perhatikan adapters = {} di atas: array ini awalnya kosong, dan akan kita isi satu per satu di bagian berikut. Aturan emasnya: setiap adapter yang kalian sebut di adapters wajib terdaftar di dependencies dengan lazy-loading per filetype — jika tidak, require("neotest-go") akan mengembalikan nil dan Neotest error di startup.

Adapter per Bahasa

Sekarang kita lengkapi config dengan adapter untuk bahasa-bahasa yang paling umum dipakai tim backend dan DevOps: Go, Python, dan JavaScript/TypeScript. Perhatikan bahwa setiap adapter ditambahkan dua kali — sekali sebagai dependency (dengan ft lazy-loading), dan sekali sebagai fungsi di adapters:

lua/plugins/neotest.lua — lengkap dengan adapter
{
  "nvim-neotest/neotest",
  event = "VeryLazy",
  dependencies = {
    "nvim-lua/plenary.nvim",
    "nvim-neotest/nvim-nio",
    "nvim-treesitter/nvim-treesitter",
    { "nvim-neotest/neotest-go", ft = "go" },
    { "nvim-neotest/neotest-python", ft = "python" },
    { "nvim-neotest/neotest-jest", ft = { "javascript", "typescript", "javascriptreact", "typescriptreact" } },
    { "marilari88/neotest-vitest", ft = { "javascript", "typescript", "javascriptreact", "typescriptreact" } },
  },
  keys = { /* keymaps yang sama seperti sebelumnya */ },
  config = function()
    require("neotest").setup({
      adapters = {
        require("neotest-go")(),
        require("neotest-python")(),
        require("neotest-jest")(),
        require("neotest-vitest")(),
      },
    })
  end,
}

Setiap adapter punya opsi yang berguna dalam situasi nyata. Mari kita bedah satu per satu.

Go (neotest-go) — membungkus go test. Opsi yang sering dipakai: experimental untuk mengaktifkan fitur baru (misalnya output yang lebih kaya), dan dap untuk debugging test dengan dlv:

Adapter Go
require("neotest-go")({
  experimental = true,
  args = { "-count=1", "-timeout=60s" },
  dap = {
    dap_open = function() end,
  },
})

Python (neotest-python) — secara default memakai pytest. Jika project memakai unittest, kalian bisa memilih runner-nya. Opsi yang paling sering dibutuhkan adalah menentukan interpreter Python yang dipakai (terutama di environment dengan virtualenv):

Adapter Python
require("neotest-python")({
  python = "python3",            -- atau path virtualenv
  pytest_discover_instances = true,
  pytest_args = { "-p", "no:cacheprovider", "--maxfail=1" },
  dap = {
    args = { "python3", "-m", "pytest", "-p", "no:cacheprovider" },
  },
})

Jest (neotest-jest) — membungkus npx jest. Konfigurasi yang paling kritis adalah jestCommand, cwd, dan env — karena di project JavaScript yang sebenarnya, jest jarang berada di PATH global:

Adapter Jest
require("neotest-jest")({
  jestCommand = "npm test --",
  env = { CI = true },
  cwd = function(path)
    -- Cari folder yang punya package.json / jest.config paling dekat
    return vim.fn.getcwd()
  end,
})

Vitest (neotest-vitest) — membungkus npx vitest run. Adapter ini mendukung mode watch dan output streaming:

Adapter Vitest
require("neotest-vitest")({
  vitestCommand = "npx vitest run",
  vitestConfigFile = "vitest.config.ts",
})

Warning

Jika project memakai Vitest, hindari memasang neotest-jest dan neotest-vitest secara bersamaan tanpa filter — kedua adapter bisa mengklaim file *.test.ts yang sama dan menyebabkan test dijalankan ganda. Solusinya: pisahkan per project dengan menetapkan adapter mana yang aktif berdasarkan lokasi file, atau cukup install adapter yang sesuai dengan stack project kalian. Jangan biarkan dua adapter bersaing untuk file yang sama.

Alur Kerja: Run, Navigasi, dan Debug

Dengan setup di atas, mari kita jalankan alur kerja yang sesungguhnya. Buka sebuah file math_test.go:

math_test.go
package main
 
import "testing"
 
func TestAdd(t *testing.T) {
	if Add(2, 3) != 5 {
		t.Fatal("expected 5")
	}
}
 
func TestMultiply(t *testing.T) {
	if Multiply(2, 3) != 6 {
		t.Fatal("expected 6")
	}
}

Letakkan kursor di dalam TestAdd, lalu tekan <leader>tr (run nearest). Neotest akan:

  1. Mengidentifikasi bahwa kalian berada di dalam TestAdd.
  2. Menjalankan go test -run TestAdd -count=1 -timeout=60s ..
  3. Menampilkan ikon status di gutter — spin/loading saat berjalan, lalu ✓ hijau atau ✗ merah.
  4. Membuka output panel secara otomatis (karena auto_close = true — panel menutup sendiri saat test lulus).

Jika test gagal, tekan <leader>to untuk melihat output detail, lalu ]n untuk melompat ke test gagal berikutnya, [n untuk kembali. Perbaiki, tekan <leader>tl (run last) untuk mengulang test yang sama tanpa harus menebak lagi.

Ketika ingin menjalankan semua test dalam satu file, <leader>tt. Untuk seluruh proyek (dari working directory), <leader>tT. Dan <leader>tw mengaktifkan watch mode — setiap kali kalian menyimpan file, test di file itu langsung dijalankan ulang otomatis, seperti vitest watch atau pytest --watch.

Summary, Output Panel, dan Command Alternatif

Ada dua cara melihat hasil: summary window dan output panel.

  • Summary window (<leader>ts) menampilkan pohon test dengan status per node — berguna saat menjalankan seluruh suite dan ingin melihat gambaran besar: berapa test pass, berapa fail, di file mana.
  • Output panel (<leader>tO) menampilkan output mentah dari runner — persis seperti yang akan kalian lihat di terminal. Ini tempat kalian membaca stack trace dan pesan assertion yang sebenarnya.

Selain keymaps, Neotest juga menyediakan command :Neotest dengan sub-perintah yang sama. Kombinasi keduanya memberi fleksibilitas: keymap untuk tindakan cepat, command untuk yang jarang dipakai.

Men-debug Test dengan Neovim DAP

Salah satu fitur paling ampuh Neotest adalah menjalankan test di bawah debugger. Di episode 22 kita sudah menyiapkan nvim-dap. Neotest memanfaatkannya melalui strategi dap — tekan <leader>td (atau :Neotest run -s dap), dan test akan dijalankan dalam mode debug:

  • Breakpoint yang kalian pasang di kode akan berhenti selama test berjalan.
  • nvim-dap-ui menampilkan variabel, call stack, dan watch — persis seperti debug test di IDE GUI.
  • Kalian bisa step over/into/out menggunakan keymap DAP yang sudah kalian buat di episode 22.
Neotest strategi DAP
-- Di dalam require("neotest").setup(...)
strategies = {
  dap = {
    presentation = "split",   -- tampilkan UI debugger di split bawah
  },
},

Perlu diingat: strategi dap hanya berguna jika adapter bahasa tersebut mendukungnya (misalnya Go butuh dlv). Untuk Python, debugpy harus tersedia. Jika debugger tidak ditemukan, Neotest akan memberi tahu kalian lewat output panel.

Tabel Keymap Neotest

Rangkuman keymap yang akan sering kalian pakai setiap hari:

KeymapFungsi
<leader>trRun test terdekat (nearest)
<leader>ttRun seluruh file test
<leader>tTRun semua test di working directory
<leader>tlRun ulang test terakhir
<leader>twToggle watch mode pada file
<leader>tsToggle summary window
<leader>toTampilkan output test terakhir
<leader>tOToggle output panel
<leader>tSStop test yang sedang berjalan
<leader>tdRun test dengan debugger (DAP)
]n / [nLompat ke test gagal berikutnya / sebelumnya

Kesalahan Umum (Common Pitfalls)

  1. Adapter tidak diinstall tapi di-require. Error attempt to call a nil value (global 'require') atau adapter tidak muncul di gutter — cek bahwa adapter ada di dependencies dan fungsi adapter dipanggil di adapters. Ini penyebab paling umum kenapa Neotest "tidak melakukan apa-apa".

  2. Test discovery gagal karena Treesitter parser tidak terinstall. Neotest butuh parser Treesitter untuk bahasa tersebut agar bisa menemukan posisi test. Buka file bahasa itu, jalankan :TSInstall go, :TSInstall python, atau :TSInstall typescript, lalu coba lagi.

  3. Working directory salah. Neotest menjalankan perintah test dari root yang dideteksi. Jika project kalian monorepo (misalnya Go workspace atau pnpm workspace), test bisa gagal karena dijalankan dari direktori yang salah. Sesuaikan opsi cwd pada adapter (seperti contoh Jest di atas) atau pastikan Neovim dibuka dari root project.

  4. Test "no tests found" padahal file test ada. Sering terjadi ketika file test tidak mengikuti konvensi adapter — misalnya file test_foo.py di luar direktori yang dikenali pytest, atau _test.go di package yang tidak punya go.mod di level yang dicari. Periksa struktur project dan konfigurasi adapter.

  5. Dua adapter mengklaim file yang sama (Jest vs Vitest). Ini menghasilkan eksekusi ganda dan hasil yang membingungkan. Pastikan hanya satu adapter aktif per stack, atau filter berdasarkan cwd/path.

  6. Runner binary tidak ada di PATH. jest, vitest, atau python3 harus bisa dipanggil dari environment Neovim. Verifikasi dengan :!which jest / :!go version. Jika memakai Node version manager (nvm), pastikan PATH-nya di-load sebelum Neovim berjalan — ini juga berpengaruh pada LSP yang dijalankan Mason.

Checklist Integrasi Testing

NoItemStatus
1neotest + plenary + nvim-nio terpasang
2Treesitter parser untuk bahasa project terinstall
3Adapter untuk setiap bahasa terdaftar di dependencies & adapters
4Tidak ada dua adapter yang mengklaim filetype yang sama
5Keymaps run/summary/output sudah terdefinisi
6Test berhasil dijalankan via <leader>tr di project nyata
7(Opsional) Debug test dengan DAP sudah teruji

Penutup

Pada episode 26 ini kita telah mengintegrasikan testing langsung ke dalam alur kerja Neovim: memasang neotest dengan arsitektur core + adapter, mengonfigurasi adapter untuk Go, Python, dan JavaScript/TypeScript, serta membangun alur kerja lengkap — dari menjalankan satu test terdekat, satu file, seluruh suite, mode watch, hingga men-debug test dengan nvim-dap. Sekarang Neovim kalian bukan lagi sekadar editor, melainkan sebuah lingkungan pengembangan yang utuh: menulis, menjalankan, dan memverifikasi kode tanpa pernah berpindah aplikasi.

Kuncinya adalah: semakin dekat test dengan alur mengetik, semakin sering test dijalankan — dan semakin kuat jaring pengaman kalian. Alur <leader>tr setelah menulis setiap perubahan akan menjadi refleks yang menyelamatkan kalian berkali-kali.

Di episode 27 — episode terakhir dari seri Belajar Neovim — kita akan merangkai seluruh perjalanan dari episode 0 hingga sekarang menjadi satu arsitektur config production-grade yang lengkap: struktur modular init.lua, options, keymaps, autocmds, plugin, LSP, completion, formatting, hingga checklist kesiapan daily-driver. Ini akan menjadi penutup yang menyatukan semuanya. Sampai jumpa di episode pamungkas!

Belajar Neovim - Testing & Code Running dengan Neotest | Belajar Neovim