0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「200 OKなのに壊れていた」ブログ記事から考えた機能テストの進め方とpytestでの自動化

0
Posted at

はじめに

数週間ほど、会社のブログ記事の一部が壊れていました。しかも誰も気づいていませんでした。

壊れていたといっても、エラー画面が出ていたわけではありません。ページは普通に表示されて、サーバーは毎回200を返していました。ただ、記事の中を見ると画像があるはずの場所が空の枠になっていて、本文の内部リンクを踏むと404。そんな状態です。

見つけたきっかけはSEO監査でした。テストでもなく、読者からの報告でもなく。クローラー系の監査ツールが出してきた一覧を見て、正直ちょっと血の気が引きました。

あとからアクセス解析を見ると、ちゃんと痕跡が残っていました。記事に来て、画像のない記事を少し読んで、リンクを踏んで404を見て、そのまま離脱。読者は「画像が切れてますよ」なんて連絡はくれません。黙って帰るだけです。

全部「動いて」はいました。でも、やるべきことはやれていなかった。

この記事では、その経験をもとに、機能テストをどう考えて、どこまで自動化して、どこを人の目で見ているかをまとめます。私はQA専任のエンジニアではなく、プロダクトやWebサイト側でユーザーに近いところを見ている立場なので、その目線で書いています。

機能テストを一言でいうと

友達に説明するなら、こう言います。

「機能テストは、それがちゃんと誰かの役に立つことをしているか確かめること。クラッシュせずに動いているか、だけじゃなくて。」

ブログ記事の役割はHTMLを返すことではありません。読者が内容を読めて、説明用の図が見えて、次の記事に進めること。どれか一つでも欠けていたら、その記事は壊れています。ステータスコードが何であっても、です。

なお、ここでは教科書的な分類(スモーク、サニティ、リグレッション、UAT…)を一行ずつ並べるのはやめておきます。あの一覧、何回読んでも覚えられないので。

ほかのテストとの違い

ざっくり整理するとこうなります。

  • ユニットテスト:関数やコンポーネント単体が正しく動くか
  • 結合テスト:複数の部品(API とDB、フロントとAPIなど)が正しくつながるか
  • 機能テスト:要件どおりの振る舞いを、外側(ユーザーと同じ入口)から確認する
  • 非機能テスト:速さ、負荷、セキュリティなど「どれだけうまく動くか」

私の意見ですが、結合テストと機能テストの境目は「いくつのサービスに触れるか」ではないと思っています。違いは、期待値がどこから来ているかです。「エンドポイントが200を返す」はコード側の期待値。「読者が記事の画像をすべて見られる」は要件側の期待値。同じツールで書けても、確かめている問いが違います。

うちのブログの場合、CMSは正常、サーバーも正常、画像ファイルもどこかには存在していました。部品はすべて合格。でもページ全体としては不合格でした。

機能バグはどこから来るのか

原因を掘っていくと、一つではありませんでした。地味な原因が重なっていた感じです。

  1. コンテンツの下で何かが変わった
    どこかのタイミングで画像のURLが変わり、記事内のリンクは古い場所を指したままでした。記事は何も変わっていないのに、周りが変わった。
  2. 誰も持ち主がいないリンク
    内部リンクは書いた人が一度書いたきり、誰も見直しません。ページはリネームされ、統合され、削除されます。去年の記事の中のリンクはそれを知りません。
  3. 「完成」の定義がなかった
    「正常な記事とは何か」を誰も書いていなかったので、自然と「表示されればOK」が定義になっていました。これはコンテンツの問題に見えて、実は要件の問題です。

原因の切り分け方や、テストの種類ごとの役割をもう少し体系的に知りたい方は、機能テストの全体像をまとめたこちらの解説がわかりやすいので、あわせて読んでみてください。

この手の損失は昔から言われていて、NISTの2002年のレポートでは、不十分なテスト基盤による米国経済の損失を年間595億ドルと見積もっています。古い数字ですが、「一応動いている」系のバグの割合が減ったとはあまり思えません。

テストケースはユーザーの一文から作る

今は、テストケースを書く前に「ユーザーが言いそうな一文」で要件を書くようにしています。たとえば「検索から記事に来た人が、画像もリンクも含めて最後まで読める」。この一文を分解したものがテストケースになります。

表にするとこんな感じです。

ID 前提条件 操作 期待結果 自動/手動
TC-01 公開済みの記事 記事URLを開く 200が返り、本文が空でない 自動
TC-02 画像を含む記事 記事を開く すべての画像が取得でき、空の枠がない 自動
TC-03 内部リンクを含む記事 本文中のリンクを開く 404にならず、意図したページに着く 自動
TC-04 URLを変更・統合した記事 旧URLにアクセス 新URLへ301で転送される 自動
TC-05 スマホ表示 記事を開いて最後までスクロール 画像・表・コードがはみ出さずに読める 手動
TC-06 検索からの流入 検索結果から記事に着地 迷わず目的の情報やボタンにたどり着ける 手動(Clarity)

ポイントは、ハッピーパスだけで終わらせないことです。古い記事、何度も編集された記事、リニューアルを生き延びた記事。今朝公開したばかりの記事だけ見ても、たぶん何も見つかりません。

pytest + requests で自動化する

TC-01〜TC-03 は自動化しやすいので、pytest と requests で書くとこうなります。HTMLの解析には BeautifulSoup を使います。

import pytest
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse

BASE = "https://example.com"
# 実際はサイトマップから記事URLを取得するのがおすすめです
POSTS = ["/blog/post-a", "/blog/post-b"]


@pytest.mark.parametrize("path", POSTS)
def test_post_works_for_readers(path):
    res = requests.get(urljoin(BASE, path), timeout=10)
    # 200はスタート地点にすぎない
    assert res.status_code == 200
    soup = BeautifulSoup(res.text, "html.parser")

    # 本文が空でないこと(テンプレートだけ返っていないか)
    article = soup.select_one("article")
    assert article and len(article.get_text(strip=True)) > 300

    # 画像が実際に取得できること
    for img in article.select("img"):
        src = urljoin(res.url, img.get("data-src") or img.get("src", ""))
        r = requests.get(src, timeout=10)
        assert r.status_code == 200, f"画像が壊れています: {src}"
        assert r.headers.get("Content-Type", "").startswith("image/"), f"画像ではありません: {src}"

    # 内部リンクが404になっていないこと
    for a in article.select("a[href]"):
        href = urljoin(res.url, a["href"])
        if urlparse(href).netloc == urlparse(BASE).netloc:
            r = requests.head(href, allow_redirects=True, timeout=10)
            assert r.status_code < 400, f"リンク切れ: {href}"

いくつか補足です。

  • 遅延読み込みの画像は src がプレースホルダーになっていることが多いので、data-src を優先して見ています。ここを忘れると誤検知だらけになります。
  • 画像のチェックで Content-Type まで見ているのは、存在しない画像に対してHTMLの404ページを200で返すサーバーがあるからです。これもまさに「200なのに壊れている」パターンです。
  • サーバーによっては HEAD に対応していないので、その場合は GET に切り替えます。

CIで回すときの注意

こうしたチェックは、CIやスケジュール実行に載せておくのがおすすめです。うちのエンジニアリング側では、実際のトラフィックからAPIテストを自動生成してCIで回しているので、APIの振る舞いはそちらでカバーし、ページ側はページとして機能しているかに絞る、という分担にしています。

気をつけたいのは不安定なテストです。Google Testing Blog(2016年)によると、Google全体でテスト実行の約1.5%が flaky な結果を報告し、テストのほぼ16%に何らかの flakiness があったそうです。Googleでもそうなので、小さなスイートでも当然起きます。外部の画像CDNが一瞬遅いだけで落ちる、みたいなことは普通にあります。タイムアウトとリトライを決めておいて、不安定なテストはその日のうちに隔離する。でないと、みんな「再実行」ボタンを押すだけになります。

自動化しないもの

まあ、ここは反論が来そうですが、私はこう思っています。

実ユーザーのセッション録画は、多くの人が書くテストケースの半分よりもたくさんの機能バグを見つける。

テストケースの多くは、開発者が想定した使い方をテストしています。でもユーザーはその通りには動きません。画像をリンクだと思ってタップしたり、フォームの途中で戻るボタンを押したり、目立つと思っていたボタンを素通りしたり。Microsoft Clarityの録画を10本見るだけで、誰もテストに書こうと思わなかったことが見えてきます。

だから「全部自動化できる」という話には賛成できません。自動化は、安定していて壊れると痛い部分に使う。それ以外は、実際の人と同じように使ってみないと見つからないものがあります。TC-05 と TC-06 を手動にしているのはそのためです。

(余談ですが、SEO監査でバグが見つかるのは、たぶん一番地味な見つかり方です。朝会で自慢できる話ではありません。)

私がやらかしたこと

1. 200が返れば動いていると思い込んでいた
ページが表示されて200が返っていれば、頭の中では「動いている」ことになっていました。でも、壊れたフォームや画像の欠けたページが200を返しても、それは壊れたページです。理屈ではわかっていたのに、ステータスだけ見て次に進んでいました。速いし、確認した気になれるので。

2. 誰かが教えてくれると思っていた
ブログの読者はバグ報告をしてくれません。怒って去るわけでもなく、静かにタブを閉じるだけです。

最後に一つだけ

表示されるページと、機能しているページは別物です。サーバーが何と言っているかではなく、ユーザーが何を必要としているかを確かめましょう。

みなさんが見た中で、一番「全部グリーンだったのに壊れていた」バグは何でしたか?そして最終的に、誰が見つけましたか?テスト、ユーザー、それとも誰も期待していなかった監査でしょうか。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?