CLIテストを安定させカバレッジを大幅向上(開発日記 No.096)
関連リンク
はじめに
前回の開発では、CLI機能の修正やテンプレート・モデル指定といった新機能の追加、そしてそれらに伴うテストの更新を行いました。しかし、テストカバレッジはまだ低く、特にCLIモジュール(cli.py
)は14%と改善の余地が大きい状況でした。
そこで本日は、このCLIモジュールのテストに集中的に取り組み、テストの安定化、設計の統一、そしてカバレッジの大幅な向上を目指しました。
背景と目的
開発中のツールは、コマンドラインインターフェース(CLI)を通じてユーザーに機能を提供します。CLIはユーザーとの主要な接点であるため、その動作が安定していることは非常に重要です。しかし、前回の開発時点ではcli.py
のカバレッジが低く、また既存のテストも不安定な部分がありました。
この不安定さの主な原因は、CLI内部でファイル出力などの副作用を持つ処理が直接記述されており、テスト時にこれらの副作用を適切に分離・モック化できていなかったことにありました。
本日の目的は、cli.py
のテストを安定させ、信頼性を高めること、そしてテストカバレッジを80%以上に引き上げることで、CLIモジュールの品質を保証することです。同時に、テストしやすい設計へとコードを改善することも目指しました。
検討内容
テストを安定させるために、まず不安定さの原因となっている箇所を特定しました。それは、CLIのmain()
関数内でファイルへの書き込み処理(open()
やwrite()
)が直接行われている点でした。このような副作用を持つ処理がテストコードから分離されていないと、テスト実行環境に依存したり、モックが複雑になったりして、テストが不安定になりがちです。
この問題を解決するため、ファイル出力処理を既に存在するconverter.py
モジュール内のsave_converted_file()
関数に統一的に任せる設計変更を検討しました。これにより、main()
関数は引数の解析と適切な関数の呼び出しに専念し、副作用を持つ処理は別の関数に委譲されるため、テストが格段に容易になります。
テストコード側では、この設計変更に合わせて、モックの適用範囲や引数の渡し方、そしてエラー発生時の経路を現行のCLI仕様に完全に一致させるよう修正する方針を立てました。特に、argparse
で定義した引数(ショートオプション含む)がテストのモック引数と一致しているか、ConverterFactory
のような依存オブジェクトが正しくモック化され、main()
関数に渡されているかを確認することが重要だと考えました。
実装内容
検討に基づき、以下の実装を行いました。
-
CLI
main()
関数の修正:cli.py
のmain()
関数内で直接行われていたファイル出力処理を削除し、代わりにconverter.save_converted_file()
関数を呼び出すように変更しました。これにより、CLIのコアロジックとファイルI/Oが分離されました。 -
テストコード (
test_cli.py
) の修正:-
mock_args
オブジェクトを、実際のargparse
が生成する名前空間オブジェクトと完全に互換性があるように調整しました。特に、ショートオプションの指定漏れがないか確認しました。 - ファイル出力に関するテストケースで、
converter.save_converted_file()
が適切な引数で呼び出されることを検証するようにモックを設定し直しました。 - エラーハンドリングに関するテストケースを、現在のCLIのエラー発生経路に合わせて修正・追加しました。例えば、ファイルが存在しない場合や不正な引数が渡された場合のエラーメッセージや終了コードを検証しました。
-
-
テスト実行とカバレッジ測定:
pytest
とpytest-cov
を使用してテストを実行しました。
pytest --cov=your_module_name
この結果、全てのテストケースがパスし、cli.py
のカバレッジが目標の80%を大きく超える84%に達しました。
- 開発ワークフローの自動化: 今回の作業とは直接関係ありませんが、開発終了時のコミット、プッシュ、プルリクエスト作成、開発記録の更新といった一連のワークフローを自動化する仕組みを整備しました。これにより、開発の効率が向上しました。
技術的なポイント
今回の開発で特に重要だった技術的なポイントは以下の通りです。
-
責務の分離: CLIの
main()
関数からファイル出力という副作用を持つ処理を切り出し、converter.save_converted_file()
に委譲したことは、コードのテスト容易性と保守性を大幅に向上させました。関数が単一の責務を持つように設計することは、テストを書く上で非常に有効です。 -
モックの正確性: テストコードにおけるモックオブジェクト(特に
mock_args
や依存クラスのモック)は、実際のオブジェクトの振る舞いを正確に模倣する必要があります。argparse
の名前空間オブジェクトの構造や、ConverterFactory
がどのようにインスタンスを生成し、それがmain()
関数内でどのように使われるかを理解し、それに合わせたモックを設定することがテスト成功の鍵でした。 -
テストと実装の同期:
argparse
で新しい引数を追加したり、既存の引数を変更したりした場合、それに対応するテストコード(特にmock_args
の定義)も忘れずに更新する必要があります。今回はショートオプションの定義漏れがテスト失敗の一因となっていたことから、この同期の重要性を再認識しました。
所感
正直なところ、CLIのテストがなかなか安定せず、苦労する場面もありました。特に、open()
やwrite()
のような副作用を持つ処理がコードのあちこちに散らばっていると、テストでそれらを適切にモック化するのが非常に難しいことを痛感しました。テストが不安定だと、コード変更のたびにテストが落ちてしまい、本当にバグなのか、テストコードが悪いのかの切り分けに時間がかかります。
しかし、今回思い切ってファイル出力処理をconverter.save_converted_file()
に統一したことで、main()
関数が非常にシンプルになり、テストコードもそれに合わせて整理することができました。結果として、テストが劇的に安定し、カバレッジも目標を大きく超える84%に達したときは、大きな達成感がありました。
この経験を通じて、テストを書くことは単にカバレッジを上げるだけでなく、コードの設計を改善する強力なフィードバックになることを改めて学びました。テストが書きにくいコードは、往々にして設計に改善の余地があるということです。
また、テストが全件パスし、高いカバレッジを達成したことで、CLIモジュールに対する自信を持つことができました。これなら、今後機能を追加したり修正したりする際も、安心して開発を進められそうです。
今後の課題
CLIモジュールのテストは安定し、カバレッジも向上しましたが、プロジェクト全体としてはまだテストカバレッジが低いモジュール(例: converter.py
)が残っています。今後はこれらのモジュールのテストを強化し、プロジェクト全体の品質を高めていく必要があります。
また、ローカル環境でのテストだけでなく、CI/CDパイプラインを構築し、コードの変更があるたびに自動的にテストが実行される仕組みを導入することも重要な課題です。これにより、早期に問題を検出し、品質保証を自動化することができます。
まとめ
本日の開発では、CLIモジュール(cli.py
)のテスト安定化とカバレッジ向上に成功しました。ファイル出力処理をconverter.save_converted_file()
に統一するという設計変更が、テストの安定化とカバレッジの大幅向上(14% → 84%)に大きく貢献しました。
この経験から、責務の分離やモックの適切な使用といったテスト容易性を考慮した設計の重要性を改めて認識しました。テストはコードの品質を保証するだけでなく、設計を改善するための強力なツールでもあります。
今後は、他のモジュールのテスト強化やCI/CDの導入を進め、プロジェクト全体の品質向上を目指していきます。
Discussion