テストサイズは、Googleがテストを「何を使って動くか」で分けるための分類です。Smallは単一プロセス、Mediumは単一マシン、Largeは制限なし。単体テスト・結合テストのような「どこまで検証するか(スコープ)」とは別の軸で、単体・結合という呼び方を置き換えるものではありません。この記事では、Googleの一次資料にある定義と、出典によって食い違う実行時間の数字の読み方、Bazel・pytest・Goでサイズを付けて実行し分ける方法を、pytest 8.4.2・Go 1.27.2で実行結果を確かめたコードつきで整理します。
まとめ
- テストサイズの境界は実行環境。Small=単一プロセス(sleep・I/O・ネットワーク不可)、Medium=単一マシン(通信はlocalhostのみ)、Large=複数マシンにまたがってよい。
- サイズとスコープは別の軸。単体テストでもローカルのDBにつなげばMedium、画面からDBまで通す広いテストでも依存をすべてプロセス内に置き換えればSmallです。
- 「80%・15%・5%」はスコープの配分(単体・結合・E2E、テストケース数ベース)で、サイズの比率ではありません。
- 時間の数字は出典で性質が違う。60秒・300秒・900秒は打ち切りの上限(Testing Blog 2010・Bazel)、200ミリ秒・1000ミリ秒は1件あたりの目安(Android)です。
- ラベルは強制して初めて効く。GoogleはSmallのネットワーク接続を失敗させる仕組みを持ちます。pytestのconftest.pyでも、socket.socket.connectを経由する接続を制限する例を実装できます。ただし、Smallの制約全体を強制するものではありません。
テストサイズの定義:Small・Medium・Largeを分ける実行環境の境界
現在の定義の出典として最もまとまっているのは、Googleのエンジニアが書いた書籍『Software Engineering at Google』の第11章「Testing Overview」(Adam Bender著、CC BY-NC-ND 4.0で公開)です。同章は3つのサイズを「small tests run in a single process, medium tests run on a single machine, and large tests run wherever they want」と1文で要約しています。同章によれば、この定義は場合によっては「テスト基盤がテストに強制できる制約」として組み込まれています。
| サイズ | 動いてよい範囲 | 通信 | sleep・I/O | 典型例 |
|---|---|---|---|---|
| Small | 単一プロセス(多くは単一スレッド) | 不可 | 不可 | 関数・クラスの検証 |
| Medium | 単一マシン(複数プロセス可) | localhostのみ | 可 | ローカルDB・ヘッドレスブラウザ |
| Large | 複数マシン | 制限なし | 可 | リモート環境へのE2E |
Smallは「テストするコードと同じプロセスで動く」ことが条件なので、サーバーを立てて別プロセスから接続するテストや、データベースを外部プログラムとして起動するテストは書けません。重い依存はテストダブル(プロセス内の軽い代替実装)に置き換えます。Mediumは複数プロセス・スレッドとブロッキング呼び出しを許しますが、localhost以外への通信は禁止です。Largeはその制限も外し、テストと対象システムが別々のマシンにあってよい区分です。
Small・Medium・Largeという呼び方は、Google社内の有志グループTesting Groupletが2006年に「unit」「regression」に代わる用語として導入したものです(当事者のMike Bland氏の記録)。社外向けには、Google Testing Blogの「Test Sizes」(2010年12月13日、Simon Stewart)が機能ごとの可否を次の表で示しました。
| 機能 | Small | Medium | Large |
|---|---|---|---|
| ネットワーク | No | localhost only | Yes |
| データベース | No | Yes | Yes |
| ファイルシステム | No | Yes | Yes |
| 外部システムの利用 | No | Discouraged | Yes |
| マルチスレッド | No | Yes | Yes |
| sleep文 | No | Yes | Yes |
| システムプロパティ | No | Yes | Yes |
| 制限時間(秒) | 60 | 300 | 900+ |
同じ記事は、サイズをJavaのアノテーションで示し、アノテーションが無いテストはSmallとして扱うと書いています。後述のBazelは逆に、サイズ未指定をmediumとみなします。既存のテストにサイズを付けていく途中では、未指定のテストがどちらに数えられるかを先に確認してください。
テストサイズとスコープ(単体・結合・E2E)の違い
サイズは「何を使って動くか」、スコープは「どこまで検証するか」
第11章は、サイズを「テストの実行に必要なリソース(メモリ・プロセス・時間)」、スコープを「検証する具体的なコードパス」と定義し、「Size and scope are interrelated but distinct concepts.」と書いています。相関はあるが別物、という意味です。
- モデルのメソッド1つを検証する単体テストでも、ローカルで起動したPostgreSQLに接続するならMedium。
- コントローラからリポジトリまで通す広いテストでも、データベースをプロセス内のフェイクに置き換えればSmall。
- UIコンポーネント1つのテストでも、実ブラウザを別プロセスで起動するならMedium。
Googleが単体・結合ではなくサイズで分ける理由について、同章は「テストスイートに最も求める性質は、スコープにかかわらず速度と決定性だから」と説明しています。名前の解釈が人によって割れる「単体」「結合」と違い、サイズはプロセス・マシンという境界で機械的に判定できます。単体テスト・結合テストという工程上の区分がなくなるわけではないので、発注者と工程を話すときは従来の呼び方、CIの実行設計ではサイズ、と使い分けるのが実務的です。工程としての区分はシステム開発のテスト工程とは?種類・流れとV字モデル・発注者が見る判断軸で整理しています。
「80%・15%・5%」はサイズではなくスコープの配分
「Googleはテストの80%をSmallにしている」という紹介を見かけますが、第11章の数字はスコープの話です。原文は、狭いスコープの単体テストを約80%、2つ以上のコンポーネントの連携を検証する中スコープの結合テストを15%、システム全体を検証するE2Eテストを5%とし、割合は「テストケース数ベースで、チームごとに少しずつ違う」と注記しています。同章はさらに、E2Eばかり多い「アイスクリームコーン」と、単体とE2Eが多く結合が薄い「砂時計」を避けるべき形として挙げています。
この配分を図にしたものがテストピラミッドです。層の設計そのものはテストピラミッドとは?単体・結合・E2Eの配分と実装者向けテスト戦略設計、結合テストを厚くする別の考え方はTesting Trophy(テスティングトロフィー)とは?Kent C. Doddsの4層とテストピラミッドとの違いで扱っています。
Bazelの説明が「単体=small」と書いている理由
ビルドツールBazelの属性定義(Common definitions)には「Unit tests are considered “small”, integration tests “medium”, and end-to-end tests “large” or “enormous”」とあり、2010年のTesting Blogも「Small test equates neatly to a unit test」と書いています。これは典型的な対応を示した説明で、第11章の「別の軸」と矛盾するものではありません。迷うのは、典型から外れたテスト(DBにつなぐ単体テスト、フェイクだけで通る広いテスト)を分類するときで、そのときの判定根拠はスコープではなくプロセスとマシンの境界です。
実行時間の目安:出典ごとに違う数字の読み分け
テストサイズの紹介記事は、時間の数字が記事ごとに違います。出典をたどると、数字の性質が「打ち切りの上限」と「1件あたりの目安」の2種類に分かれます。
| 出典 | Small | Medium | Large | 数字の性質 |
|---|---|---|---|---|
| Testing Blog 2010 | 60秒 | 300秒 | 900秒以上 | 制限時間 |
| Bazel(既定timeout) | 60秒 | 300秒 | 900秒 | 打ち切り(上書き可) |
| Android(androidx) | 200ミリ秒未満 | 1000ミリ秒未満 | 1000ミリ秒超 | 1件の実行時間 |
BazelのTest Encyclopediaでは、timeoutはshort 60秒・moderate 300秒・long 900秒・eternal 3600秒で、sizeを指定するとそれぞれsmall→short、medium→moderate、large→long、enormous→eternalが暗黙に割り当てられます。Common definitionsはさらに、ローカル実行時に見込むピークRAMをsmall 20MB・medium 100MB・large 300MB・enormous 800MB(CPUはいずれも1コア)と定めています。ここでのsizeは、同時に何本のテストを走らせるかを決めるスケジューリングの材料でもあります。
Androidのandroidx.test.filters.SmallTestは「Execution time: <200ms」、MediumTestは「<1000ms」、LargeTestは「>1000ms」と1件あたりの時間で書いています。Smallは「ファイルシステム・ネットワーク・データベースへのアクセス不可」、MediumはContentProviderなど定められたインターフェース経由のファイルアクセスを許す、とリソースの条件も添えられています。
実務で決めるべきなのは2つです。CIで打ち切る上限(Bazelではテストターゲット全体に適用され、実行環境に応じて調整します)と、Smallが遅くなってきたことに気づくための1件あたりの目安です。60秒は「この秒数までなら許す」ではなく「これを超えたら壊れている」という線です。Bazelも、フレークを起こさない範囲でtimeoutをできるだけ短く設定するよう勧めています。実際の実行時間と宣言したサイズがずれているテストは、--test_verbose_timeout_warningsを付けて実行すると警告として一覧に出ます。Test Encyclopediaはtimeoutごとの推奨下限(short 0秒・moderate 30秒・long 300秒・eternal 900秒)も示しており、5.5秒で終わるmoderateのテストならtimeout = "short"かsize = "small"への変更を検討するよう書いています。
迷うケースのサイズ判定
判定は大きいほうから順に確かめると迷いません。
- localhost以外のマシンに通信するか → するならLarge
- 別プロセスを起動するか、sleep・ファイルI/O・ブロッキング呼び出しを使うか → 使うならMedium
- どちらも無い → Small
| テストの中身 | サイズ | 根拠 |
|---|---|---|
| リポジトリをインメモリのフェイクに置換 | Small | プロセス内で完結 |
| SQLiteの:memory:で検証 | Small | ライブラリがプロセス内で動き、ディスクを使わない |
| 一時ファイルに書いて読み戻す | Medium | ファイルI/O |
| 同一マシンのPostgreSQLコンテナ(localhost接続) | Medium | 別プロセス・localhost通信 |
| ヘッドレスChromeでの画面操作 | Medium | 別プロセスのブラウザ |
| ステージング環境のAPIを呼ぶ | Large | 別マシンへの通信 |
| 外部SaaSのサンドボックスで決済 | Large | 外部ネットワーク |
DBコンテナをテストと同一マシン上で動かし、通信をlocalhost内に収めればMediumに分類できます。リモートのDocker環境を使う場合はLargeです。使い捨てのDBコンテナの立て方はTestcontainersとは?Dockerで作る使い捨てテスト環境と2.0系の移行点、Smallに置き換えるときの代替実装の種類はテストダブル(TestDouble)とは|5分類の違いと使い分けを参照してください。
テストサイズをコードに付けて実行し分ける方法
Bazel:size属性と–test_size_filters
Bazelはテストターゲットのsize属性でサイズを宣言します。値はsmall・medium・large・enormousで、既定はmediumです。
load("@rules_python//python:py_test.bzl", "py_test")
py_test(
name = "tax_test",
srcs = ["tax_test.py"],
size = "small",
)
実行時はコマンドラインリファレンスにある--test_size_filtersで絞ります。カンマ区切りで指定し、先頭に「-」を付けると除外です。
# smallだけを実行
bazel test --test_size_filters=small //...
# largeとenormousを除いて実行
bazel test --test_size_filters=-large,-enormous //...
テストの中からは環境変数TEST_SIZEで自分のサイズを参照できます。flaky = Trueを付けると初回を含め最大3回実行されますが、Common definitionsはこの属性の使用を「generally discouraged」としています。
pytest:サイズマーカーの登録と-mによる実行対象の指定
Bazelを使っていないプロジェクトでも、テストランナーのタグ機能でサイズを付けられます。pytestではマーカーを登録し、--strict-markersで綴り間違いのマーカーをエラーにしておきます。
# pytest.ini
[pytest]
addopts = --strict-markers
markers =
small: 単一プロセスで完結(ネットワーク・ディスク・sleep禁止)
medium: 単一マシン内(localhostのみ通信可)
large: 複数マシンにまたがってよい
pytest:socket.socket.connect経由の接続制限
マーカーを付けただけでは、Smallのテストにネットワーク接続が紛れ込んでも気づけません。第11章によると、GoogleはJavaのテストをカスタムのsecurity manager上で動かし、smallと付いたテストが禁止された操作(ネットワーク接続など)をすると失敗させています。pytestでは、autouseのfixtureでsocket.socket.connectを差し替えると、そのメソッドを経由する接続を検出できます。ただし、Googleの制約強制と同等ではありません。
# conftest.py
import socket
import pytest
_real_connect = socket.socket.connect
def _deny_connect(self, *args, **kwargs):
raise RuntimeError("smallテストからのネットワーク接続は禁止")
def _localhost_only(self, address, *args, **kwargs):
host = address[0] if isinstance(address, tuple) else address
if host not in ("127.0.0.1", "::1", "localhost"):
raise RuntimeError(f"mediumテストはlocalhost以外へ接続できない: {host}")
return _real_connect(self, address, *args, **kwargs)
@pytest.fixture(autouse=True)
def enforce_size(request, monkeypatch):
if request.node.get_closest_marker("small"):
monkeypatch.setattr(socket.socket, "connect", _deny_connect)
elif request.node.get_closest_marker("medium"):
monkeypatch.setattr(socket.socket, "connect", _localhost_only)
# test_sizes.py
import socket
import pytest
def add_tax(price, rate=0.10):
return round(price * (1 + rate))
@pytest.mark.small
def test_add_tax():
assert add_tax(1000) == 1100
@pytest.mark.small
def test_small_cannot_connect():
with pytest.raises(RuntimeError):
socket.create_connection(("192.0.2.1", 80), timeout=1)
@pytest.mark.medium
def test_medium_localhost_only():
srv = socket.socket(); srv.bind(("127.0.0.1", 0)); srv.listen()
c = socket.create_connection(srv.getsockname(), timeout=1)
c.close(); srv.close()
with pytest.raises(RuntimeError):
socket.socket().connect(("192.0.2.1", 80))
pytest 8.4.2(Python 3.9.6)で実行した結果です。-mでサイズを指定すると、対象外のテストは「deselected」として数えられます。
$ python3 -m pytest -m small -q
2 passed, 1 deselected
$ python3 -m pytest -m "small or medium" -q
3 passed
conftest.pyを外して同じコマンドを実行すると、test_small_cannot_connectは接続を試みたままsocket.timeoutで失敗しました(1 failed, 1 passed)。ラベルだけでは、Smallのテストに混ざった通信を止められないということです。
この方法で防げるのは、fixture適用中にsocket.socket.connectを経由する接続だけです。connect_ex、UDP送信、別プロセスの通信、fixture適用前の処理は対象外です。ファイルI/Oやsleepまで禁止したい場合は、同じ要領でopenやtime.sleepを差し替えて対象APIの呼び出しを検出します。通信の遮断は別途CIのネットワーク設定で行い、ファイルI/Oやsleepの検出とは分けて設計します。
Go:ビルドタグによるMedium以上の実行対象の分離
Goでは、Medium以上のテストファイルにビルドタグを付けると、タグを渡したときだけコンパイル・実行されます。タグなしのgo testがSmallだけを回す形になります。Small相当のテストはタグを付けないファイルに置きます。
// tax.go
package sizes
func AddTax(p int) int { return p * 110 / 100 }
// tax_test.go(タグなし=Small)
package sizes
import "testing"
func TestAddTax(t *testing.T) {
if AddTax(1000) != 1100 {
t.Fatal("税込価格が違う")
}
}
Medium以上のテストは別ファイル(ここではmedium_test.go)の先頭にビルドタグを書きます。
//go:build medium
package sizes
import (
"net"
"testing"
)
func TestLocalServer(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
c, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
c.Close()
}
go mod init sizesで作ったモジュールに3ファイルを置き、Go 1.27.2で実行しました。タグなしのgo test -v ./...はTestAddTaxだけを実行し、go test -v -tags medium ./...ではTestLocalServerとTestAddTaxの両方が実行されました。
Android:@SmallTest・@MediumTest・@LargeTest
Androidはサイズ用のアノテーションを公式に持っています。androidx.test.filtersの3つを付けておけば、AndroidJUnitRunnerに-e size smallのように渡してサイズ単位で実行できます。旧パッケージandroid.test.suitebuilder.annotationの同名アノテーションは非推奨で、androidx版に置き換わっています。
adb shell am instrument -w -e size small com.example.app.test/androidx.test.runner.AndroidJUnitRunner
CI/CD:テストサイズ別の実行頻度の設計
サイズを付ける目的は、遅いテストと速いテストを同じタイミングで回さないことです。Smallはすべてのイベント、Mediumはプルリクエスト、Largeは夜間、という分け方が基本形になります。GitHub Actionsでpytestのマーカーを使う場合の例です。各サイズのテストと実行に必要な依存関係が用意されている前提で、Smallのジョブはpush・pull_request・scheduleのすべてで動きます。-m largeに当てはまるテストが1件も無いと、pytestは全件deselectedのまま終了コード5を返し、ジョブは失敗します(pytest 8.4.2で確認)。
name: tests
on:
push:
pull_request:
schedule:
- cron: "0 18 * * *"
jobs:
small:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install pytest
- run: python -m pytest -m small
medium:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install pytest
- run: python -m pytest -m medium
large:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install pytest
- run: python -m pytest -m large
cronはUTCなので、「0 18 * * *」は日本時間の午前3時です。Largeを夜間に回す理由は時間だけではありません。第11章によると、同書の記述時点では、Googleのフレーク(コードに問題が無いのに結果が揺れる)率は約0.15%で、毎日数千件のフレークが起きていました。同章は、フレーク率が1%に近づくとテストが価値を失い始めると書いています。Largeを一律にマージ判定から外すのではなく、重大な回帰を検知する安定したテストはゲートに残します。不安定なテストは原因調査と修正の担当・期限を決めて一時的に隔離し、夜間の結果を担当者が確認する運用にしておくと、開発者がテスト失敗を無視する習慣が付きにくくなります。CI/CDの構成全体はCI/CDとは:仕組み・パイプライン構成・GitHub Actionsの設定例と導入判断の基準を解説で、パイプラインへの組み込みを外部に任せる場合はDevOps・CI/CD導入支援で相談を受け付けています。
Bazelでモノレポ全体を管理しているなら、同じ分け方を--test_size_filtersで実現できます。Bazelで単一リポジトリを管理する運用はモノレポ(Monorepo)とは?マルチレポとの違い・メリット・デメリットと導入手順にまとめています。
テストサイズを導入しても効かない場面
テストサイズの導入は、件数ではなく、CIの待ち時間やフレーク、外部依存を分けて管理する必要性で判断します。次の状況では、ラベルを付ける手間のほうが大きくなります。
- 全テストが数分で終わり、フレークも起きていない。実行頻度を分ける必要がなく、サイズを付けても運用は何も変わりません。まずスコープの偏り(E2Eばかり、など)が無いかを見るほうが先です。
- ラベルを付けるだけで強制しない。Smallと書かれたテストに後からDB接続が混ざっても誰も気づかず、数か月でラベルが実態と合わなくなります。上のconftest.pyのような検査か、ネットワークを遮断した環境での実行をセットにしてください。
- Smallを増やすためにモックを積み上げる。呼び出し順まで固定するモックが増えると、実装を少し変えただけでテストが壊れます。Smallにしにくい処理はフェイクかコンテナを使ったMediumに置くほうが、保守の手間は小さくなります。
- 外部サービスとの接続そのものを確かめたいテストをMediumに落とす。スタブは相手の仕様変更を検知できません。外部APIの認証や応答形式の変化を捕まえる目的のテストは、本数を絞ってLargeに残します。
- Bazelのflaky属性で再実行に頼る。3回中1回通れば成功扱いになるため、フレークの原因が放置されます。
よくある質問
テストサイズと単体テスト・結合テストは同じものですか?
別の軸です。単体・結合・E2Eは検証するコードの広さ(スコープ)、テストサイズは実行に使う範囲(単一プロセス・単一マシン・制限なし)を表します。多くの単体テストはSmallですが、ローカルのDBにつなぐ単体テストはMediumです。
Mediumテストとは何を指しますか?
単一のマシン内で完結するテストです。複数プロセス・スレッド、sleep、ファイルI/Oを使えますが、通信できるのはlocalhostだけです。ローカルで起動したDBやコンテナ、ヘッドレスブラウザを使うテストが該当します。
Enormousというサイズはありますか?
Bazelにはsmall・medium・largeに加えてenormousがあり、既定のtimeoutは3600秒(eternal)、見込むRAMは800MBです。Googleの書籍とTesting Blogの説明はSmall・Medium・Largeの3つが中心です。
サイズを指定しないテストはどう扱われますか?
Bazelでは既定がmediumで、timeoutは300秒になります。2010年のTesting Blogでは、アノテーションの無いテストをSmallとして扱うと説明しています。ツールによって逆なので、導入時に未指定の扱いを確認してください。
Googleはテストの何割をSmallにしていますか?
『Software Engineering at Google』第11章と2010年のTesting Blogには、サイズ別の比率は書かれていません。よく引用される80%・15%・5%は、同章が示すスコープ(単体・結合・E2E)の目安で、テストケース数ベースの大まかな指針です。