👋

エディタ内でテスト結果が表示される開発体験を、エディタに依存せず実現するツールを作った

2024/10/15に公開

エディタ上でテストのエラーを表示することができるLSPサーバとその周辺ツールを作りました。

https://github.com/kbwo/testing-language-server

動機

数ヶ月前にこの記事を見ました。

Wallaby.jsを使ってフロントエンド開発のテストを効率化しよう - Findy Tech Blog https://tech.findy.co.jp/entry/2024/04/15/100523

エディタ上でリアルタイムにテスト結果が反映される開発体験が大変魅力的に見えます。
私は普段Neovimを使用しているので、この記事を見てすぐ、Neovimでも同じようなことがやりたい、と考えました。Wallaby.jsはNeovim用の拡張は用意していないようだったので、その時点で選択肢からは外れていたのですが、Wallaby.jsについて詳しく調べたところ、どうもそのツールはJavaScriptのテストフレームワークに合わせた細かいチューニングが行われているようでした。
特定のツールに合わせて細かいチューニングが行われたツールは、条件さえ合えばパフォーマンスやユーザビリティにおいて非常に優れているでしょう。
ただ、私は「より汎用的に、言語やエディタに縛られずに使える、あるいは拡張できるフレームワーク」が欲しいと考えました。

同じように、エディタ上でテスト結果を反映する機能を実現しているものとして、
https://github.com/nvim-neotest/neotest
があります。
これは私の考えていた、汎用的に、どんな言語に対しても使える、あるいは拡張できるという要望を満たしています。
ただし、これにも私が好まない仕様がありました。それは、Neovimに依存しているという点です。私のメインのエディタはNeovimですし、他のエディタを使うことはほとんどありません。ただ、私はNeovimに固執せず、機会があれば積極的に他のエディタも触りたいタイプです。Wallaby.jsがVSCodeに縛られているのと同じように、neotestがNeovimに縛られているのは私の思想に合いませんでした。

そこで、私はエディタに縛られないツールを自作しようと考えました。
エディタに縛られず、標準化されている実装をするということで真っ先に浮かんだのはLSPでした。私が目指すところとする、"汎用性"を備えたLSPサーバも、以下に挙げるようにいくつかすでに実装例があります。

エディタに縛られずに機能を提供できるLSPであれば、NeovimでもVSCodeでもほぼ同一の機能が実現できるでしょうし、LSPのdiagnosticsとしてテストのエラーが表示されるのも十分に便利ではないかと考えて作ってみました。

作ったもの

VS Code

coc.nvim

このように、エディタ上で編集後、保存したときにテストを実行してエラーがあれば診断 (diagnostics) を提供することでエラーを表示します。

現在は、以下のテストフレームワークに対応しています。

  • cargo test
  • cargo nextest
  • jest
  • deno test
  • go test
  • phpunit
  • vitest

インストール方法

LSPサーバのインストール

まだバイナリ配布はしていないため、cargo installでインストールする必要があります。
cargoコマンドが実行できない人は、rustupをインストールすることでcargoコマンドを使えるようにしてください。

cargo install testing-language-server
cargo install testing-ls-adapter

エディタごとの設定

VSCode

https://marketplace.visualstudio.com/items?itemName=kbwo.testing-language-server から拡張機能をインストールしてください。

coc.nvim

:CocInstall coc-testing-lsで拡張機能をインストールしてください。

Neovim builtin LSP (nvim-lspconfig)

以下のような設定を書きます。このような設定を書かずに済むように、いずれnvim-lspconfigにPRを作ります。参考: https://github.com/kbwo/testing-language-server/tree/main/demo#readme
※現状はNeovim builtin LSP対応は他に比べて一番貧弱で、リアルタイムなテスト実行による診断のみサポートしています。今後やりたいことの部分にも記載していますが、その他の便利コマンドを使えるようにするために今後プラグインを用意する予定です。

local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
local util = require "lspconfig/util"

configs.testing_ls = {
  default_config = {
    cmd = { "testing-language-server" },
    filetypes = { "rust" },
    root_dir = util.root_pattern(".git", "Cargo.toml"),
      init_options = {
        enable = true,
        fileTypes = {"rust"},
        adapterCommand = {
		  -- プロジェクトごとに実行するテストを設定を参照
		  -- ここではRustのプロジェクトであることが前提の設定を記載している
          rust = {
            {
              path = "testing-ls-adapter",
              extra_arg = { "--test-kind=cargo-test", "--workspace" },
              include = { "/demo/**/src/**/*.rs"},
              exclude = { "/**/target/**"},
            }
          }
        },
        enableWorkspaceDiagnostics = true,
        trace = {
          server = "verbose"
        }
      }
  },
  docs = {
    description = [[
      https://github.com/kbwo/testing-language-server

      Language Server for real-time testing.
    ]],
  },
}

lspconfig.testing_ls.setup{}

プロジェクトごとに実行するテストを設定

.vscode/settings.json.vim/coc-settings.jsonのように、各エディタ環境でプロジェクトごとの設定を記載することで、それに応じたテスト実行ができます。この設定を正しくしないとこのLSPサーバは期待通りに動きません。

実例を見たほうが早いと思うので、以下を参照してください。
VSCodeの設定例:
https://github.com/kbwo/testing-language-server/blob/main/demo/.vscode/settings.json

coc.nvimの設定例:
上はtesting-language-serverを直接使う場合の設定例、下はcoc-testing-lsを介して使う場合の設定例です。
https://github.com/kbwo/testing-language-server/blob/main/demo/.vim/coc-settings.json
https://github.com/kbwo/testing-language-server/blob/main/.vim/coc-settings.json

このLSPサーバでは、以下のようなadapterCommandの設定が肝です。

"adapterCommand": {
	  "cargo-test": [
		{
		  "path": "testing-ls-adapter",
		  "extra_arg": ["--test-kind=cargo-test"],
		  "include": ["/**/src/**/*.rs"],
		  "exclude": ["/**/target/**"]
		}
	  ],
	  "cargo-nextest": [
		{
		  "path": "testing-ls-adapter",
		  "extra_arg": ["--test-kind=cargo-nextest"],
		  "include": ["/**/src/**/*.rs"],
		  "exclude": ["/**/target/**"]
		}
	  ],
	  "jest": [
		{
		  "path": "testing-ls-adapter",
		  "extra_arg": ["--test-kind=jest"],
		  "include": ["/jest/*.js"],
		  "exclude": ["/jest/**/node_modules/**/*"]
		}
	  ],
	  "vitest": [
		{
		  "path": "testing-ls-adapter",
		  "extra_arg": ["--test-kind=vitest"],
		  "include": ["/vitest/*.test.ts", "/vitest/config/**/*.test.ts"],
		  "exclude": ["/vitest/**/node_modules/**/*"]
		}
	  ],
	  "deno": [
		{
		  "path": "testing-ls-adapter",
		  "extra_arg": ["--test-kind=deno"],
		  "include": ["/deno/*.ts"],
		  "exclude": []
		}
	  ],
	  "go": [
		{
		  "path": "testing-ls-adapter",
		  "extra_arg": ["--test-kind=go-test"],
		  "include": ["/**/*.go"],
		  "exclude": []
		}
	  ],
	  "phpunit": [
		{
		  "path": "testing-ls-adapter",
		  "extra_arg": ["--test-kind=phpunit"],
		  "include": ["/**/*Test.php"],
		  "exclude": ["/phpunit/vendor/**/*.php"]
		}
	  ]
  }

これは、プロジェクト内の様々なファイルタイプに対して、どのようにテストをするかを定義します。
各キー(cargo-testjestなどの部分)ははテスト方法を識別する任意のキーなので好きに設定して構いません。
設定オブジェクトのプロパティを見てみましょう:

  • path: アダプターの実行ファイルへのパス。
  • extra_arg: アダプターに渡す追加の引数の配列です。
    • ここで重要なのは--test-kind=<runner>で、これはアダプターにどのタイプのテストを扱うかを伝えます。
      • 現在は上の例で書いた種類のみがサポートされています
    • testing-ls-adapterを使用せず、別のアダプターを使用する場合は不要な場合もあると思います。
      • 詳しくは設計の項目を参照
  • include: テスト対象に含めるファイルを指定するグロブパターンの配列です。
  • exclude: テスト対象から除外するファイルやディレクトリを指定するグロブパターンの配列です。
    • includeより常にexcludeが優先されます。

設計

ツールの構成

今回作ったツールの構成は3つに分けられます。LSPサーバー (testing-language-server)、アダプター(testing-ls-adapter)、そして各エディタ(VSCodeやcoc.nvim)用の拡張です。それぞれがどのような役割をしているのかを説明します。

testing-language-server:

  • Language Server Protocol (LSP)を実装したメインのサーバーです。
  • エディタやIDEとの通信を担当し、テスト関連のエディタコマンドや診断(diagnostics)をエディタに送信する機能を提供します。

testing-ls-adapter:

  • これは、特定のテストフレームワークやツールとtesting-language-serverの間のブリッジとして機能するアダプターです。
  • 各テストフレームワーク(例:Cargo Test、Jest、Vitest、Deno、Go Test、PHPUnit)に特化した実装を提供するCLIです。

各エディタ用の拡張:

  • testing-language-serverとの通信を担当するLSPクライアントです。
  • 診断(diagnostics)の提供だけでなく、testing-language-serverを使うにあたって便利なコマンドを提供します。
    • ファイルレベル、ワークスペースレベルのテストの手動実行や、不要な診断のクリアなど。

なぜアダプターの実装をLSPサーバー内に入れなかったのか

ただ今回のツールでできることを実装するだけなら、LSPサーバーに直接、各テストフレームワークに特化した実装を盛り込んで、LSPサーバーだけで動作させることができました。にも関わらず、なぜ実装を分けたかというと、"動機"の項目にも記載した、このツールの構想段階で参考にしたneotestの設計に影響を受けたからです。
neotestでも同様に、コアな機能実装と、各テストフレームワークへの対応を別々にしています。アダプターはコア部分が定義したインターフェイスさえ実装していればどのようなテストフレームワークにも対応できるようになっています。

同じ設計を、このツールではadapterをCLIとして設計することで実現しています。特定のインターフェイスを実装したアダプターであれば、任意の値をadapterCommandで設定できます。(実装しなければならないインターフェイスについてのドキュメントもあります。まだ十分な記述にはなっていませんが。)

adapterを分離し、CLIとして設計することで、アダプターは誰でも、好きな言語やツールを使って実装することができます。現在は私がいくつかのテストフレームワークへの対応をtesting-ls-adapterを通して行っていますが、必ずしもそれを使う必要はなく、自由にアダプターを書けます。誰かがこのツールを使ってくれるのかもわからないのでここまでの拡張性が必要だったのかは少し怪しいですが、少なくとも私自身は、アダプターの実装時に言語に縛られないことで、ライブラリ等も自由に選択できるのが嬉しいと感じています。
ちなみに、アダプターの実装をLSPサーバーから独立させ、実装する言語に縛られないようにするための実装方法を考えたとき、RPCやwasmを活用する実装方針も考えましたが、慎重に検討した結果、やりたいことに対して一番シンプルな実装にできるのがCLIであると判断しました。

今後やりたいこと

ドッグフーディングして、自分が不満に思ったところを改善していきます。

今のところやる予定の改善は以下です。

  • より多くのテストフレームワークへの対応
  • 各エディタ用の拡張の充実
    • VSCodeのテスト実行環境周りへのintegration
    • Neovim Builtin LSP用の対応をするプラグイン
    • 便利コマンドの追加
  • より効率的なテスト実行やキャッシュ機能
  • ドキュメントの充実
    • 実はtesting-ls-adapter経由でjestなどのテストコマンドを実行するときに環境変数を渡せたりするのですが、ドキュメントを書いていなくて暗黙の機能になっています。
    • 自分でもよく、何が設定できるのか忘れるので必ずまとめます。

追記

予想以上に反響をいただきました。ありがとうございます。
このLSPサーバーとその周辺ツールはとてもwell-testedとは言えない状態なので、もし使っていただいたうえで不具合や不便があれば、積極的にissueを立ててもらえると助かります。

Discussion