開発未分類
リンターとテストの違い - Pythonカスタムリンター作成を例に
結論:リンターとテストは「いつ」「何を」検査するかが違う
本記事では「リンター」を静的解析ツールの代表例として扱う。厳密には、静的解析には型検査やデータフロー解析なども含まれ、リンターはその一部である。
| 項目 | リンター(静的解析の一種) | テスト(動的検証) |
|---|---|---|
| 実行タイミング | コードを実行せずに検査 | コードを実際に実行して検査 |
| 検査対象 | コードの書き方・パターン | コードの動作・出力 |
| 検出できること | 誤字、危険なパターン、スタイル違反 | バグ、期待と異なる出力、エッジケース |
| 速度(一般的な傾向) | 速い場合が多い | 実行が必要な分、遅い場合が多い |
※速度は解析の種類や規模により逆転することもある。ユニットテストは高速で、大規模な静的解析は重いケースもある。
具体例で理解する
リンターが検出する問題
# リンターは「この書き方は危険」と警告する
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
リンターはコードを実行せず、構文解析やパターンマッチで「f-stringでSQLを組み立てている」ことを検出する。実際にSQLインジェクションが起きるかどうかは関係なく、危険な書き方そのものを指摘する。
テストが検出する問題
def calculate_tax(amount):
return amount * 0.1
# テストコード
def test_calculate_tax():
assert calculate_tax(1000) == 100 # 期待: 100
assert calculate_tax(0) == 0 # 期待: 0
テストはコードを実際に実行して、入力に対する出力が期待通りかを検証する。
実践:プロジェクト固有のカスタムリンター作成
税理士業務アプリ「Tax Assistant」用に、プロジェクト固有のルールを持つリンターを作成した。
検出ルール
| ルール | 検出内容 | 理由 |
|---|---|---|
shiwake-typo | 「仕分け」→「仕訳」の誤字 | 会計用語は「仕訳」が正しい |
sql-injection | execute()にf-stringを渡しパラメータなし | SQLインジェクション脆弱性 |
no-float-money | 金額計算でfloat使用 | 浮動小数点誤差を防ぐため |
hardcoded-secret | パスワード・APIキーのハードコード | セキュリティリスク |
実装例:仕訳誤字検出
def check_shiwake_typo(filename: str, source: str) -> Iterator[LintError]:
"""「仕分け」の誤字を検出。正しくは「仕訳」"""
for i, line in enumerate(source.splitlines(), start=1):
matches = list(re.finditer(r"仕分け", line))
for match in matches:
yield LintError(
file=filename,
line=i,
column=match.start(),
rule="shiwake-typo",
message="「仕分け」→「仕訳」の誤字です",
severity="error",
)
このチェッカーは:
- ファイルを1行ずつ読み込む
- 正規表現で「仕分け」というパターンを探す
- 見つかったら行番号と位置を記録
実際にPythonコードとして実行するのではなく、テキストとしてパターンマッチしている。
実装例:SQLインジェクション検出(AST解析)
class RawSQLChecker(ast.NodeVisitor):
"""execute()にf-stringを渡し、パラメータなしの場合のみ検出"""
def visit_Call(self, node: ast.Call) -> None:
if isinstance(node.func, ast.Attribute) and node.func.attr == "execute":
if node.args:
first_arg = node.args[0]
has_params = len(node.args) >= 2
# f-string でパラメータなしの場合のみ警告
if isinstance(first_arg, ast.JoinedStr) and not has_params:
self.errors.append(LintError(...))
Pythonのastモジュールを使うと、コードを構文木として解析できる。これにより:
execute()という関数呼び出しを特定- 第1引数がf-stringかどうかを判定
- 第2引数(パラメータ)があるかどうかを確認
構文木を静的に解析しているだけで、対象コードを実行しているわけではない。
誤検知(False Positive)との戦い
最初のリンターは22件のエラーを検出したが、確認すると多くが誤検知だった。
# 誤検知の例:このコードは安全
where_clause = f"WHERE {' AND '.join(conditions)}"
rows = conn.execute(f"""
SELECT * FROM users
{where_clause}
""", params) # ← paramsでパラメータを渡している
このコードはf-stringを使っているが、実際の値はparamsで安全に渡している。また、where_clauseの構造部分(列名やAND演算子)はコード内で固定されており、ユーザー入力由来ではない。
「パラメータ渡しがある場合は除外」するロジックを追加した結果、22件 → 4件に減少した。
注意: SQL構造部分(列名、ORDER BY、演算子など)にユーザー入力を使う場合は、パラメータ化できないため別途ホワイトリスト検証が必要。
リンターとテストの使い分け
リンターが得意なこと
- コーディングスタイルの統一
- セキュリティリスクのあるパターン検出
- プロジェクト固有のルール適用(用語の統一など)
- 高速なフィードバック(CI/CDで毎回実行)
テストが得意なこと
- ビジネスロジックの検証
- エッジケースの確認
- リファクタリング後の動作保証
- 期待する入出力の明文化
両方使うのがベストプラクティス
開発フロー:
1. コード編集
2. リンター実行 → 書き方の問題を即座に検出
3. テスト実行 → 動作の問題を検出
4. コミット
まとめ
- リンター:コードを実行せず、構文解析やパターンマッチで「書き方」を検査(静的解析の一種)
- テスト:コードを実行して、「動作」を検証
- プロジェクト固有のルールはカスタムリンターで実装できる
- 誤検知を減らすには、検出ロジックの精度向上が必要
- リンターとテストは役割が異なるので、両方使うのがベスト