Skip to content

Contributing

Contributions are welcome

Improvement suggestion PRs to this repo are very much welcome, and I encourage you to begin by reading the below paragraph on the adapter design and engage in the discussions in case the change is not trivial.

You can run tests, formatting and linting locally with task all (requires Taskfile). Install dependencies with task install. Have a look at the Taskfile.yml for more details. You can also use the neotest-plenary and neotest-golang adapters to run the tests of this repo within Neovim. Please refer to the Test setup for detail on how to run tests.

AST and tree-sitter

To figure out new tree-sitter queries (for detecting tests), the following commands are available in Neovim to aid you:

  • :Inspect to show the highlight groups under the cursor.
  • :InspectTree to show the parsed syntax tree (formerly known as "TSPlayground").
  • :EditQuery to open the Live Query Editor (Nvim 0.10+).

For example, open up a Go test file and then execute :InspectTree. A new window will appear which shows what the tree-sitter query syntax representation looks like for the Go test file.

Again, from the Go test file, execute :EditQuery to open up the query editor in a separate window. In the editor, you can now start creating your syntax query and play around. You can paste in queries from query.lua in the editor, to see how the query behaves and highlights parts of your Go test file.

Required query captures

Neotest expects specific capture names in tree-sitter queries to build test positions. The core library (neotest/lua/neotest/lib/treesitter/init.lua) looks for these captures:

For tests:

  • @test.name - The name/identifier of the test (required)
  • @test.definition - The AST node representing the entire test definition (required)

For namespaces (e.g., testify suites):

  • @namespace.name - The name/identifier of the namespace (required)
  • @namespace.definition - The AST node representing the entire namespace definition (required)

The build_position function in Neotest's treesitter library extracts text from the .name capture and uses the .definition capture's range to determine the position. These captures are then converted into neotest.Position objects with these fields:

  • type - Position type: "test" or "namespace" (or "file" / "dir")
  • path - Absolute file path
  • name - Test or namespace name (extracted from .name capture)
  • range - Array of line/column positions from .definition capture's range

Additional captures like @test.method, @test.operand, etc., can be used within queries for predicates and logic but are not directly consumed by Neotest's position builder.

Previewing the documentation

Install uv with e.g. brew install uv or pip install uv. Then:

  • Run uv venv to create a .venv folder
  • Activate the virtual environment with source .venv/bin/activate
  • Run uv sync to install dependencies

Finally, run uv run mkdocs serve to serve the documentation and preview it on http://localhost:8000.

General design of the adapter

Treesitter queries detect tests

Neotest leverages treesitter AST-parsing of source code to detect tests. This adapter supplies queries so to figure out what is considered a test.

From the result of these queries, a Neotest "position" tree is built (can be visualized through the "Neotest summary"). Each position in the tree represents either a dir, file or test type. Neotest also has a notion of a namespace position type, but this is ignored by default by this adapter (but leveraged to supply testify support).

Generating valid go test commands

The dir, file and test tree position types cannot be directly translated over to Go so to produce a valid go test command. Go primarily cares about a Go package's import path, test name regexp filters and the current working directory.

For example, these are all valid go test command:

Bash
# run all tests, recursing sub-packages, in the current working directory.
go test ./...

# run all tests in a given package 'x', by specifying the full import path
go test github.com/fredrikaverpil/neotest-golang/x

# run all tests in a given package 'x', recursing sub-packages
go test github.com/fredrikaverpil/neotest-golang/x/...

# run _some_ tests in a given package, based on a regexp filter
go test github.com/fredrikaverpil/neotest-golang -run "^(^TestFoo$|^TestBar$)$"

Note on go.mod

All the above commands must be run somewhere beneath the location of the go.mod file specifying the module name, which in this example is github.com/fredrikaverpil/neotest-golang.

I figured out that by executing go list -json ./... in the go.mod root location, the output provides valuable information about test files/folders and their corresponding Go package's import path. This data is key to being able to take the Neotest/treesitter position type and generate a valid go test command for it. In essence, this approach is what makes neotest-golang so robust.

Output processing

Neotest captures the stdout from the test execution command and writes it to disk as a temporary file. The adapter is responsible for reading the file(s) and reporting back status and output to the Neotest tree (and specifically the position in the tree which was executed). It is therefore crucial for outputting structured data, which in this case is done with go test -json.

One challenge here is that Go build errors are not always part of the structured JSON output (although captured in the stdout) and needs to be looked for in other ways.

Another challenge is to properly populate statuses and errors into the corresponding Neotest tree position. This becomes increasingly difficult when you consider running tests in a recursive manner (e.g. go test -json ./...).

Errors are recorded and populated, per position type, along with its corresponding buffer's line number. Neotest can then show the errors inline as diagnostics.

I've taken an approach with this adapter where I record test outcome for each Neotest position type and populate it onto each of them, when applicable.

On some systems and terminals, there are great issues with the go test output. I've therefore made it possible to make the adapter rely on output saved directly to disk without going through stdout, by leveraging gotestsum.

With neotest-golang v2.0, streaming support was added and required a total rewrite of the output processing. This made the adapter logic easier to follow and formally defined the processing steps more clearly.

When you run tests via neotest-golang, the following happens:

  • Runspec preparation: go list -json gathers package data and creates a lookup (mapping.lua) mapping Neotest positions to Go tests.
  • Streaming execution (results_stream.lua):
  • Go test JSON events are processed in real-time as they arrive.
  • Results are cached directly for immediate feedback.
  • Test output files are written synchronously when tests complete.
  • Finalization (results_finalize.lua):
  • Stops streaming and transfers cached results atomically.
  • Performs aggregation for file/directory nodes in the Neotest tree.
  • All test output is already available from the streaming phase.