🎉

動的テストをしたかったので Django の template html で変数を補完する自作拡張機能にテストを行い公開してみた

2024/02/02に公開

作ったもの

  1. Django-html-completioner という Django のviews.pyで定義している辞書のキーをrender関数の引数をもとにしてtemplate html編集時に補完する。
  2. template htmlを開いたときにviews.pyでそのファイルを呼んでいるかを確認してポップアップで表示する。
    書かれていない場合ポップアップメッセージからviews.pyを開くことができる。

上記の 2 点を含む拡張機能を開発しました
以下のように動作します

動作動画

左のviews.pyファイルで定義されている変数が右のdjango-htmlファイル編集時にサジェストされているのがお分かりいただけるかと思います。

作ったもののリンク

以下のリンクからダウンロードできます。

https://marketplace.visualstudio.com/items?itemName=yossuli.django-html-completioner

リポジトリは以下のリンクになります。
プログラミングを本格的に初めてまだ 1 年と未熟者なのでつたないコードですが、読んでいただきアドバイスなどいただけると本当に助かります。
つよつよな方よろしくお願いします...

https://github.com/yossuli/Django-html-completioner

開発経緯

大学で Django の講義が始まった際、template htmlで使える変数が何なのかは毎回views.pyを見に行かなければ分からないのが不満でした。(以前クライアントサーバーシステムのアプリケーションをサークルで作成したときは TypeScript でフロントもバックも開発していたため型による補完があった)
ほかのサークルメンバーとtemplate htmlviews.py で変数が補完されてほしいよねという話になり、Django で大きめなアプリケーションを開発している先輩にそのような機能のある拡張機能があるか聞いてみたところ知らないということだったので、作ってみようということになりました。
ちょうどそのころ動的テストをしてみたいという欲があったため、公開に向けて動的テストをすれば丁度良いということで、いろいろ調べて行く中でsinonJSを触ってみたくなったので 2 の機能を追加してポップアップに対してもテストを行いました。

仕組みや工夫

template htmlへの補完のためにviews.pyを抽象構文木解析

当初は抽象構文木というものを知らなかったためファイルをただの文字列として正規表現などをつかって解析しようとしていたのですがか特に python 特有のインデントによってのみ階層構造が変化するということをうまく処理できず、調べたところ抽象構文木にたどり着きました。
ANTLR4 ベースのパーサージェネレーターである dt-python-parserがあるのも調べた中で知ってはいたのですが、自分自身が英語が弱く使い方を理解できなかったことと、自分で抽象構文木をひも解いていく作業がかなり楽しかったため解析関数は自作することにしました。
ミュータブルな変数がtargetsを、イミュータブルな変数がtargetを持っているというのが分かったのが特に面白かったです。
実装は以下の通りです。

  1. python 標準のastライブラリを使用し、抽象構文木にパースし、自作関数で json に変換する関数を定義
  2. execSyncで 定義した python 関数を実行し json を受け取る

https://github.com/yossuli/Django-htnl-completioner/blob/main/src/utils/AST/pythonFileToASTObject.ts

  1. python 公式ドキュメントをもとに定義した型をもとに解析

https://github.com/yossuli/Django-htnl-completioner/blob/main/src/types/AST.ts

  1. render関数で、第 2 引数が現在編集中のdjango-htmlファイルであるものを検索
  2. 第 3 引数の辞書の宣言を再帰して階層を降りながら確認して該当する辞書オブジェクトのキーのリストを取得
  3. django-htmlファイル編集時の予測変換にキーのリストを追加

https://github.com/yossuli/Django-htnl-completioner/blob/main/src/extensions/djangoHTMLCompletionItemProvider.ts#L32-L39

execSyncを使用しシェルコマンドとして python を実行していたり、定義した型がドキュメントからそれっぽくなるように適当な関数で自動的に定義したもののため命名がよくなかったりなど突っ込みどころも多々ありますが、テストで予期している動作をすることを確認しているのでよしとしています。
python のコードが 1 行になっているのは Windows 環境でexecSyncを実行する際に複数行のコマンドを実行できないというバグ?に遭遇したためです。

template htmlでのポップアップ表示

こちらはメインの機能ではなく、動的テストをポップアップに対して行いたかったためつけた機能です。
一つの拡張機能にまとめるべきではなかったとは思いますが、テストを頑張ったので許してください。
実装は以下の通りです。

  1. ../../../views.pyを文字列としてthis/file.htmlがあるかを検索しポップアップメッセージを表示するコマンドを定義

https://github.com/yossuli/Django-htnl-completioner/blob/main/src/extensions/checkRenderingCommand.ts#L1-L43

  1. template htmlを開いたときに 1 のコマンドを実行
  2. 検索してなかった場合にポップアップメッセージのボタンを押すと../../../views.pyを開く

テストで動作を担保

もともとやりたかったのが動的テストをしてみるということなので簡単ではありますがテストを書きました。
(E2E テストというものらしいですね。)

テストケースを for 文で回しながら動作を確認しています。

https://github.com/yossuli/Django-htnl-completioner/blob/main/src/test/extension.test.ts

テスト対象のディレクトリは以下の通りです。

.
└── djangoApp
     ├── templates
     │   └── djangoApp
     │       ├── index.html
     │       ├── index2.html
     │       ├── index3.html
     │       └── index4.html
     └── views.py
def index(request):
    if a:
        if b:
            return render(request,'djangoApp/index3.html')
        context={'test2_1':'value','test2_2':'value'}
        return render(request,'djangoApp/index2.html',context)
    context={'test1':'value'}
    return render(request,'djangoApp/index.html',context)

機能 1 のテスト

テストの流れとしては

  1. 別プロセスで vscode を立ち上げ、指定したファイルを開く
  2. 実際にファイルにカーソルが置かれ、その位置での補完のリストを取得する
  3. 取得されたリストが予期しているものと一致しているかを確認
  4. vscode のクリーンアップ

をテストケースの分だけ行います。

テストの詳細は以下の通りです

https://github.com/yossuli/Django-htnl-completioner/blob/main/src/test/utils/suites/completionItemsTestInVscode.ts

以下の部分でテスト対象のファイルにカーソルをあてて、補完のリストを取得しています。

const completionList =
  await vscode.commands.executeCommand<vscode.CompletionList>(
    "vscode.executeCompletionItemProvider",
    editor.document.uri,
    new vscode.Position(0, 0)
  );

その後はリストの長さやアイテムが正しいかを確認しています
テストを試行錯誤しながら書いている際、各テストケースごとの実行が割り込んでいるような感じがあったので、テストケースごとにコンソールログの色を変えながら試していったところ無事に解決して直列にテストを実行できるようになりました。

機能 2 のテスト

テストの流れとしては

  1. 別プロセスで vscode を立ち上げる
  2. sinonJS を使いメソッドの実行を監視したり stub 化したりする
  3. 指定したファイルを開く
  4. 予期した通りにメソッドが実行されているかをを確認
  5. vscode のクリーンアップ

をテストケース分だけ行っています。

テストの詳細は以下の通りです

https://github.com/yossuli/Django-htnl-completioner/blob/main/src/test/utils/suites/popupMessageTestInVscode.ts

以下の部分で sinonJS を使いメソッドの実行を監視したり stub 化したりするようにしています。

showWarningMessageStub = sinon.stub(vscode.window, "showWarningMessage");
showWarningMessageStub.returns(
  new Promise((resolve) => resolve("views.pyを開く"))
);
showInformationMessageSpy = sinon.spy(vscode.window, "showInformationMessage");
openTextDocumentSpy = sinon.spy(vscode.workspace, "openTextDocument");
showTextDocumentSpy = sinon.spy(vscode.window, "showTextDocument");

showWarningMessageStub.returnsをこのようにすることでポップアップメッセージのボタンを押したときの処理をテストできるようになりました。
その後は監視したメソッドが正しい引数で呼ばれているかや呼ばれた回数を確認しています。

反省点

途中で言語サーバー拡張機能の存在を知った時点でそっちに切り替えればよかったと思っています。(そうすれば Windows 環境特有のexecSyncの挙動に悩まされなくて済んだので)
他にも途中変なバグに遭遇したことや学校の期末試験があったせいもありますが開発速度、というか学習速度があまりにも遅く、テストをどうにかするのに 2 か月近くかかってしまたのはよくなかったです。
新しい技術を学ぶ際はきちんとドキュメントを読むべきだなぁと思いました。
(とにかく試して動かしてみてから考えている節がありまして…)

参考にした記事

以下に参考になった記事を列挙していきます。
先人の方々が残してくださった知識と経験のおかげで僕のような未熟なものでも思い通りのものを完成させることができました。
本当に感謝しかないです。

  1. この記事で vscode 拡張機能が案外簡単に作れることを知れました。

https://zenn.dev/rikuto13ten/articles/546c9bb3354a29

  1. テストについての記述が非常に参考になりました。

https://zenn.dev/ryo_kawamata/articles/development-vscode-extensions

  1. GitHub Actions について非常に参考になりました。

https://qiita.com/shun198/items/14cdba2d8e58ab96cf95

  1. 公開に向けての手順が非常に参考になりました。

https://zenn.dev/daifukuninja/articles/13a35a8bb3a4a1

  1. deploy.ymlの記述が非常に参考になりました。

https://zenn.dev/seelog/articles/article_commando

Discussion