diff --git a/.busted b/.busted
new file mode 100644
index 0000000..3b51f1b
--- /dev/null
+++ b/.busted
@@ -0,0 +1,17 @@
+return {
+ _all = {
+ coverage = false,
+ lua = "./test/nvim-shim",
+ },
+ default = {
+ verbose = true,
+ },
+ unit = {
+ ROOT = { "./test/unit/" },
+ verbose = true,
+ },
+ -- e2e = {
+ -- ROOT = {'./test/e2e/'},
+ -- pattern = '', -- No fancy names for E2E tests
+ -- },
+}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..a1db568
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,30 @@
+name: test
+on: [pull_request, workflow_dispatch]
+jobs:
+ lua:
+ strategy:
+ matrix:
+ version: [stable, nightly]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Neovim setup
+ uses: rhysd/action-setup-vim@v1
+ with:
+ neovim: true
+ version: ${{ matrix.version }}
+ - name: Set up pkgx with LuaJIT and LuaRocks
+ uses: pkgxdev/setup@v1
+ with:
+ +: luajit.org@2 luarocks.org
+ - name: Install neotest
+ run: luarocks --lua-dir=$HOME/.pkgx/luajit.org/v2 --lua-version=5.1 install neotest
+ - name: Install nvim-treesitter
+ run: luarocks --lua-dir=$HOME/.pkgx/luajit.org/v2 --lua-version=5.1 install nvim-treesitter
+ - name: Install busted
+ run: luarocks --lua-dir=$HOME/.pkgx/luajit.org/v2 --lua-version=5.1 install busted
+ - name: Load nvim-treesitter plugin and run TSInstall
+ run: nvim --headless -c 'packadd nvim-treesitter' -c 'TSInstallSync all' -c 'qa'
+ - name: Run tests
+ run: make unit-test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a76deee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+test/xdg
diff --git a/.luarc.json b/.luarc.json
new file mode 100644
index 0000000..8c8662d
--- /dev/null
+++ b/.luarc.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
+ "workspace.ignoreDir": ["test/xdg"]
+}
diff --git a/.nvimrc b/.nvimrc
new file mode 100644
index 0000000..c7a30dc
--- /dev/null
+++ b/.nvimrc
@@ -0,0 +1,2 @@
+" Use the custom shim as the busted binary for testing
+let g:bustedprg='./test/busted'
diff --git a/.pkgx.yml b/.pkgx.yml
new file mode 100644
index 0000000..999e0ca
--- /dev/null
+++ b/.pkgx.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - luajit.org
+ - luarocks.org
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5950d87
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+.PHONY: check unit-test e2e-test clean
+
+check: unit-test e2e-test
+
+unit-test:
+ @./test/busted --run unit
+
+# e2e-test:
+# @./test/busted --run e2e
+
+clean:
+ @rm -rf test/xdg/local/state/nvim/*
+ @rm -rf test/xdg/local/share/nvim/site/pack/testing/start/nvim-treesitter/parser/*
+ @# The symlink might have been left over from a failed test run
+ @rm -rf test/xdg/local/share/nvim/site/pack/self-*
diff --git a/test/busted b/test/busted
new file mode 100755
index 0000000..f1b7ce3
--- /dev/null
+++ b/test/busted
@@ -0,0 +1,34 @@
+#!/bin/sh
+# SPDX-License-Identifier: Unlicense
+
+# This is free and unencumbered software released into the public domain.
+#
+# Anyone is free to copy, modify, publish, use, compile, sell, or distribute
+# this software, either in source code form or as a compiled binary, for any
+# purpose, commercial or non-commercial, and by any means.
+#
+# In jurisdictions that recognize copyright laws, the author or authors of
+# this software dedicate any and all copyright interest in the software to
+# the public domain. We make this dedication for the benefit of the public
+# at large and to the detriment of our heirs and successors. We intend this
+# dedication to be an overt act of relinquishment in perpetuity of all
+# present and future rights to this software under copyright law.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# For more information, please refer to
+
+# A shim which acts as a command-line interface adapter for the busted test
+# framework. If busted is installed using LuaRocks we cannot invoke it
+# directly, but some tools might want to do so. This thin adapter can be used
+# as a drop-in replacement for the busted executable.
+
+export LUA_PATH="./lua/?.lua;;"
+export LUA_CPATH="./lua/?.so;;"
+
+eval $(luarocks path --lua-version 5.1 --bin) && $HOME/.luarocks/bin/busted $@
diff --git a/test/nvim-shim b/test/nvim-shim
new file mode 100755
index 0000000..d53b4cf
--- /dev/null
+++ b/test/nvim-shim
@@ -0,0 +1,71 @@
+#!/bin/sh
+# SPDX-License-Identifier: Unlicense
+
+# This is free and unencumbered software released into the public domain.
+#
+# Anyone is free to copy, modify, publish, use, compile, sell, or distribute
+# this software, either in source code form or as a compiled binary, for any
+# purpose, commercial or non-commercial, and by any means.
+#
+# In jurisdictions that recognize copyright laws, the author or authors of
+# this software dedicate any and all copyright interest in the software to
+# the public domain. We make this dedication for the benefit of the public
+# at large and to the detriment of our heirs and successors. We intend this
+# dedication to be an overt act of relinquishment in perpetuity of all
+# present and future rights to this software under copyright law.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# For more information, please refer to
+
+
+# A shim which acts as a command-line interface adapter to use Neovim as a Lua
+# interpreter.
+
+# Set custom XDG base directory paths to isolate the test Neovim from the
+# user's own configuration and data files.
+export XDG_CONFIG_HOME='test/xdg/config/'
+export XDG_STATE_HOME='test/xdg/local/state/'
+export XDG_DATA_HOME='test/xdg/local/share/'
+
+# Handle Lua command-line arguments; not all options are supported
+while getopts 'ilEve:' opt; do
+ case $opt in
+ e) lua_expr=$OPTARG;;
+ v) nvim --version; exit;;
+ i | l | E) echo "Option '$opt' not supported by shim"; exit 1;;
+ esac
+done
+
+
+# We need to add this plugin to the custom configuration. The easiest way is
+# to create a symlink. Why not always have a symlink in the project? The Lua
+# language server will search for Lua files in every directory, so if it enters
+# the symlink it will be trapped in a cycle. What we do instead is create the
+# symlink only for the duration of a test session and remove it again
+# afterwards.
+
+# We need separate symlinks if we want to run different tasks in parallel.
+# Otherwise the one the finishes first would delete the symlink from underneath
+# the one that is still running.
+uuid=$(uuidgen)
+mkdir -p ${XDG_DATA_HOME}/nvim/site/pack/self-${uuid}/start/
+ln -fs $(pwd) ${XDG_DATA_HOME}/nvim/site/pack/self-${uuid}/start/
+
+if [ -n "$lua_expr" ]; then
+ nvim --headless -c "lua $lua_expr" -c 'quitall!'
+else
+ # We have to explicitly enable plugins, see ':h -l'
+ nvim --cmd 'set loadplugins' -l $@
+fi
+
+exit_code=$?
+
+rm -rf ${XDG_DATA_HOME}/nvim/site/pack/self-${uuid}/
+
+exit $exit_code
diff --git a/test/unit/is_test_file_spec.lua b/test/unit/is_test_file_spec.lua
new file mode 100644
index 0000000..1372e5b
--- /dev/null
+++ b/test/unit/is_test_file_spec.lua
@@ -0,0 +1,18 @@
+local adapter = require("neotest-golang")
+
+describe("Is test file", function()
+ it("True - Path to file", function()
+ local file_path = "foo/bar/baz_test.go"
+ assert.is_true(adapter.is_test_file(file_path))
+ end)
+
+ it("True - Just filename", function()
+ local file_path = "foo_test.go"
+ assert.is_true(adapter.is_test_file(file_path))
+ end)
+
+ it("False - Not a test file", function()
+ local file_path = "foo_bar.go"
+ assert.is_false(adapter.is_test_file(file_path))
+ end)
+end)