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.

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.
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:
go test -run TestFoo, pytest test_foo.py, npm test -- --runInBand).Ini pola yang sama dengan extension test runner di IDE modern — tapi berjalan di dalam Neovim yang sudah kalian kenal.
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).
{
"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.
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:
{
"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:
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):
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:
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:
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.
Dengan setup di atas, mari kita jalankan alur kerja yang sesungguhnya. Buka sebuah file 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:
TestAdd.go test -run TestAdd -count=1 -timeout=60s ..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.
Ada dua cara melihat hasil: summary window dan output panel.
<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.<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.
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:
nvim-dap-ui menampilkan variabel, call stack, dan watch — persis seperti debug test di IDE GUI.-- 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.
Rangkuman keymap yang akan sering kalian pakai setiap hari:
| Keymap | Fungsi |
|---|---|
<leader>tr | Run test terdekat (nearest) |
<leader>tt | Run seluruh file test |
<leader>tT | Run semua test di working directory |
<leader>tl | Run ulang test terakhir |
<leader>tw | Toggle watch mode pada file |
<leader>ts | Toggle summary window |
<leader>to | Tampilkan output test terakhir |
<leader>tO | Toggle output panel |
<leader>tS | Stop test yang sedang berjalan |
<leader>td | Run test dengan debugger (DAP) |
]n / [n | Lompat ke test gagal berikutnya / sebelumnya |
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".
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.
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.
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.
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.
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.
| No | Item | Status |
|---|---|---|
| 1 | neotest + plenary + nvim-nio terpasang | ☐ |
| 2 | Treesitter parser untuk bahasa project terinstall | ☐ |
| 3 | Adapter untuk setiap bahasa terdaftar di dependencies & adapters | ☐ |
| 4 | Tidak ada dua adapter yang mengklaim filetype yang sama | ☐ |
| 5 | Keymaps run/summary/output sudah terdefinisi | ☐ |
| 6 | Test berhasil dijalankan via <leader>tr di project nyata | ☐ |
| 7 | (Opsional) Debug test dengan DAP sudah teruji | ☐ |
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!