はじめに#
「テストケースは書いたのに、本番で予想外の入力でバグが発生した」という経験はないでしょうか。手動で考えたテストケースには限界があり、開発者が想定しなかったエッジケースを見落としがちです。
プロパティベーステスト(Property-Based Testing)は、この問題を解決するための革新的なテスト手法です。テストフレームワークが自動的に数百から数千のテストデータを生成し、コードの「性質(プロパティ)」が常に成り立つかどうかを検証します。
本記事では、JavaScript(TypeScript)のfast-checkとJavaのjqwikを使って、プロパティベーステストの基本から実践的な活用パターンまでを解説します。
プロパティベーステストとは#
プロパティベーステストとは、特定の入力に対する期待結果ではなく、あらゆる入力に対して成り立つべき「性質(プロパティ)」を検証するテスト手法です。1999年にHaskellの「QuickCheck」で生まれた概念で、現在では多くの言語にポートされています。
従来のテスト(Example-Based Testing)との違い#
従来のテスト(Example-Based Testing)とプロパティベーステストの違いを比較してみましょう。
| 観点 |
Example-Based Testing |
Property-Based Testing |
| テストデータ |
開発者が手動で選定 |
フレームワークが自動生成 |
| カバレッジ |
選定したケースのみ |
数百〜数千パターンを網羅 |
| 検証内容 |
具体的な期待値 |
普遍的な性質(プロパティ) |
| エッジケース |
開発者が意識して追加 |
自動的に境界値を生成 |
| バグ発見時 |
失敗した入力のみ判明 |
最小の再現ケースに縮約(Shrinking) |
Example-Based Testの例#
まず、従来のExample-Based Testを見てみましょう。
JavaScript(Jest):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// reverse.test.js - Example-Based Test
describe('reverseArray', () => {
test('配列 [1, 2, 3] を反転すると [3, 2, 1] になる', () => {
expect(reverseArray([1, 2, 3])).toEqual([3, 2, 1])
})
test('空配列を反転しても空配列のまま', () => {
expect(reverseArray([])).toEqual([])
})
test('要素1つの配列を反転しても同じ', () => {
expect(reverseArray([42])).toEqual([42])
})
})
|
このテストは具体的な入力と期待値のペアを検証しています。しかし、無数にある配列の組み合わせのうち、たった3パターンしか検証していません。
Property-Based Testの例#
同じ関数をプロパティベーステストで検証してみましょう。
JavaScript(fast-check):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// reverse.property.test.js - Property-Based Test
import fc from 'fast-check'
describe('reverseArray', () => {
test('2回反転すると元の配列に戻る', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const reversed = reverseArray(arr)
const doubleReversed = reverseArray(reversed)
return JSON.stringify(arr) === JSON.stringify(doubleReversed)
})
)
})
test('反転しても長さは変わらない', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
return reverseArray(arr).length === arr.length
})
)
})
test('反転しても要素は保持される', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const reversed = reverseArray(arr)
return arr.every((item) => reversed.includes(item))
})
)
})
})
|
このテストでは、fast-checkが自動的に100〜1000パターンの配列を生成し、それぞれに対してプロパティが成り立つかを検証します。開発者が思いつかなかったエッジケースも含まれます。
プロパティベーステストの核心概念#
プロパティベーステストを理解するために、3つの核心概念を押さえましょう。
flowchart TD
subgraph concepts["プロパティベーステストの3つの核心概念"]
C1["Property<br/>(性質)"]
C2["Arbitrary / Generator<br/>(データ生成器)"]
C3["Shrinking<br/>(縮約)"]
end
C1 --> D1["あらゆる入力で成り立つべき普遍的な法則"]
C2 --> D2["ランダムなテストデータを自動生成する仕組み"]
C3 --> D3["失敗時に最小の再現ケースを特定する機能"]Property(性質)#
プロパティとは、あらゆる入力に対して成り立つべき普遍的な法則です。以下のような観点でプロパティを見つけることができます。
| 観点 |
例 |
プロパティ |
| 可逆性(Round-trip) |
エンコード/デコード |
decode(encode(x)) === x |
| 冪等性(Idempotency) |
ソート |
sort(sort(xs)) === sort(xs) |
| 不変条件(Invariant) |
配列操作 |
長さ、要素の保持 |
| 相関関係 |
比較関数 |
x < y ならば y > x |
| テストオラクル |
新実装 vs 旧実装 |
同じ入力で同じ出力 |
Arbitrary / Generator(データ生成器)#
Arbitrary(fast-checkの用語)またはGenerator(jqwikの用語)は、ランダムなテストデータを自動生成する仕組みです。プリミティブ型から複雑なオブジェクトまで、様々なデータを生成できます。
Shrinking(縮約)#
Shrinkingは、テストが失敗したときに、最小の再現ケースを自動的に特定する機能です。例えば、長さ100の配列でテストが失敗した場合、Shrinkingによって「実は長さ2の配列でも再現する」ということを発見できます。
fast-checkによるプロパティベーステスト(JavaScript/TypeScript)#
fast-checkは、JavaScript/TypeScript向けのプロパティベーステストライブラリです。2025年現在、最新バージョンは4.5.3で、jestやmocha、vitestなど主要なテストフレームワークと統合できます。
環境構築#
fast-checkをプロジェクトに追加します。
npm:
1
|
npm install --save-dev fast-check
|
yarn:
1
|
yarn add --dev fast-check
|
pnpm:
基本的な使い方#
fast-checkの基本的な使い方を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import fc from 'fast-check'
describe('加算関数のプロパティ', () => {
// 交換法則: a + b === b + a
test('加算は交換法則を満たす', () => {
fc.assert(
fc.property(fc.integer(), fc.integer(), (a, b) => {
return add(a, b) === add(b, a)
})
)
})
// 結合法則: (a + b) + c === a + (b + c)
test('加算は結合法則を満たす', () => {
fc.assert(
fc.property(fc.integer(), fc.integer(), fc.integer(), (a, b, c) => {
return add(add(a, b), c) === add(a, add(b, c))
})
)
})
// 単位元: a + 0 === a
test('0を加えても値は変わらない', () => {
fc.assert(
fc.property(fc.integer(), (a) => {
return add(a, 0) === a
})
)
})
})
|
fc.assert()とfc.property()の組み合わせがfast-checkの基本パターンです。
主要なArbitrary(データ生成器)#
fast-checkは豊富なArbitraryを提供しています。
プリミティブ型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 数値
fc.integer() // 整数
fc.integer({ min: 0, max: 100 }) // 範囲指定
fc.float() // 浮動小数点数
fc.double() // 倍精度浮動小数点数
// 文字列
fc.string() // ランダム文字列
fc.string({ minLength: 1, maxLength: 10 }) // 長さ指定
fc.hexaString() // 16進数文字列
fc.emailAddress() // メールアドレス形式
fc.uuid() // UUID形式
// 真偽値
fc.boolean()
|
コレクション型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 配列
fc.array(fc.integer()) // 整数の配列
fc.array(fc.integer(), { minLength: 1, maxLength: 5 }) // サイズ制約
// オブジェクト
fc.record({
name: fc.string(),
age: fc.integer({ min: 0, max: 120 }),
email: fc.emailAddress()
})
// タプル
fc.tuple(fc.integer(), fc.string(), fc.boolean())
// Set/Map
fc.uniqueArray(fc.integer()) // ユニーク要素の配列
|
特殊なArbitrary:
1
2
3
4
5
6
7
8
9
10
11
12
|
// 固定値から選択
fc.constantFrom('apple', 'banana', 'cherry')
// 1つの固定値
fc.constant(42)
// nullableな値
fc.option(fc.integer()) // integer | null
// 日付
fc.date()
fc.date({ min: new Date('2020-01-01'), max: new Date('2025-12-31') })
|
カスタムArbitraryの作成#
独自のデータ型に対するArbitraryを作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// Userオブジェクトを生成するカスタムArbitrary
const userArbitrary = fc.record({
id: fc.uuid(),
name: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.emailAddress(),
age: fc.integer({ min: 0, max: 120 }),
isActive: fc.boolean(),
roles: fc.array(fc.constantFrom('admin', 'user', 'guest'), { minLength: 1, maxLength: 3 })
})
// mapを使った変換
const positiveEvenArbitrary = fc
.integer({ min: 1, max: 50 })
.map((n) => n * 2) // 正の偶数を生成
// filterを使ったフィルタリング(使いすぎに注意)
const oddArbitrary = fc.integer().filter((n) => n % 2 !== 0)
// chainを使った依存関係のあるデータ生成
const listWithItemArbitrary = fc.array(fc.integer(), { minLength: 1 }).chain((arr) =>
fc.record({
list: fc.constant(arr),
selectedItem: fc.constantFrom(...arr) // 配列内の要素から選択
})
)
|
前提条件(Precondition)の指定#
特定の条件を満たす入力のみをテストしたい場合は、fc.pre()を使います。
1
2
3
4
5
6
7
8
9
|
test('割り算でゼロ除算を除外する', () => {
fc.assert(
fc.property(fc.integer(), fc.integer(), (a, b) => {
fc.pre(b !== 0) // bが0の場合はスキップ
const result = divide(a, b)
return result * b === a
})
)
})
|
失敗時のShrinking#
テストが失敗すると、fast-checkは自動的に最小の再現ケースを特定します。
1
2
3
4
5
6
7
|
test('文字列が常に100文字未満(意図的に失敗させる例)', () => {
fc.assert(
fc.property(fc.string({ maxLength: 200 }), (str) => {
return str.length < 100 // 100文字以上で失敗
})
)
})
|
失敗時の出力例:
Error: Property failed after 42 tries
Counterexample: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
Shrunk 15 time(s)
Got error: Property failed by returning false
fast-checkは200文字のランダム文字列から始めて、最小の失敗ケース(ちょうど100文字の"aaa...a")まで縮約しています。
jqwikによるプロパティベーステスト(Java)#
jqwikは、Java向けのプロパティベーステストフレームワークです。JUnit 5のテストエンジンとして動作し、2025年現在の最新バージョンは1.9.3です。
環境構築#
Gradle:
1
2
3
4
5
6
7
8
9
|
dependencies {
testImplementation 'net.jqwik:jqwik:1.9.3'
}
test {
useJUnitPlatform {
includeEngines 'jqwik'
}
}
|
Maven:
1
2
3
4
5
6
|
<dependency>
<groupId>net.jqwik</groupId>
<artifactId>jqwik</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>
|
基本的な使い方#
jqwikの基本的な使い方を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import net.jqwik.api.*;
import static org.assertj.core.api.Assertions.*;
class AdditionPropertiesTest {
// 交換法則: a + b === b + a
@Property
boolean additionIsCommutative(@ForAll int a, @ForAll int b) {
return add(a, b) == add(b, a);
}
// 結合法則: (a + b) + c === a + (b + c)
@Property
void additionIsAssociative(@ForAll int a, @ForAll int b, @ForAll int c) {
assertThat(add(add(a, b), c)).isEqualTo(add(a, add(b, c)));
}
// 単位元: a + 0 === a
@Property
boolean zeroIsIdentity(@ForAll int a) {
return add(a, 0) == a;
}
}
|
@Propertyアノテーションでプロパティテストを定義し、@ForAllでパラメータへのデータ注入を指定します。
主要なArbitrary(データ生成器)#
jqwikも豊富なArbitraryを提供しています。
プリミティブ型と制約アノテーション:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 整数(範囲指定)
@Property
boolean intRangeExample(@ForAll @IntRange(min = 0, max = 100) int n) {
return n >= 0 && n <= 100;
}
// 文字列(長さ・文字種指定)
@Property
boolean stringConstraints(
@ForAll @StringLength(min = 1, max = 10) @AlphaChars String s
) {
return s.length() >= 1 && s.length() <= 10;
}
// 正の数
@Property
boolean positiveExample(@ForAll @Positive int n) {
return n > 0;
}
// 負の数
@Property
boolean negativeExample(@ForAll @Negative int n) {
return n < 0;
}
|
コレクション型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// リスト
@Property
boolean listExample(@ForAll List<@IntRange(min = 0, max = 100) Integer> numbers) {
return numbers.stream().allMatch(n -> n >= 0 && n <= 100);
}
// サイズ制約付きリスト
@Property
boolean sizedListExample(@ForAll @Size(min = 1, max = 5) List<String> items) {
return items.size() >= 1 && items.size() <= 5;
}
// Set
@Property
boolean setExample(@ForAll Set<Integer> numbers) {
// Setなので重複なし
return true;
}
// Map
@Property
boolean mapExample(@ForAll @Size(max = 10) Map<String, Integer> map) {
return map.size() <= 10;
}
|
カスタムArbitraryの作成(@Provide)#
@Provideアノテーションを使って、カスタムArbitraryを定義できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class CustomArbitraryTest {
@Property
boolean userEmailContainsAt(@ForAll("validUsers") User user) {
return user.email().contains("@");
}
@Provide
Arbitrary<User> validUsers() {
Arbitrary<String> names = Arbitraries.strings()
.alpha()
.ofMinLength(1)
.ofMaxLength(50);
Arbitrary<String> emails = Arbitraries.emails();
Arbitrary<Integer> ages = Arbitraries.integers()
.between(0, 120);
return Combinators.combine(names, emails, ages)
.as(User::new);
}
record User(String name, String email, int age) {}
}
|
前提条件(Assumptions)#
jqwikではAssume.that()で前提条件を指定します。
1
2
3
4
5
6
7
|
@Property
void divisionProperty(@ForAll int a, @ForAll int b) {
Assume.that(b != 0); // bが0の場合はスキップ
int result = divide(a, b);
assertThat(result * b).isEqualTo(a);
}
|
失敗時のShrinking#
jqwikも自動的にShrinkingを行います。
1
2
3
4
|
@Property
boolean stringAlwaysShort(@ForAll @StringLength(max = 200) String s) {
return s.length() < 100; // 意図的に失敗させる
}
|
失敗時の出力例:
Property [stringAlwaysShort] falsified with sample {0="aaaa...(100文字)"}
Shrunk Sample (15 steps)
------------------------
s: "aaaa...(ちょうど100文字のa)"
Original Sample
---------------
s: "xK3m...(150文字のランダム文字列)"
プロパティの見つけ方#
プロパティベーステストで最も難しいのは、適切なプロパティを見つけることです。以下のパターンを参考にしてください。
Round-trip(往復変換)パターン#
エンコード/デコード、シリアライズ/デシリアライズなど、可逆な変換に適用できます。
1
2
3
4
5
6
7
8
9
10
11
12
|
// JSON: parse(stringify(x)) === x
test('JSON変換は可逆', () => {
const jsonableArbitrary = fc.object()
fc.assert(
fc.property(jsonableArbitrary, (obj) => {
const serialized = JSON.stringify(obj)
const deserialized = JSON.parse(serialized)
return JSON.stringify(obj) === JSON.stringify(deserialized)
})
)
})
|
Idempotency(冪等性)パターン#
同じ操作を複数回適用しても結果が変わらない性質です。
1
2
3
4
5
6
|
@Property
boolean sortIsIdempotent(@ForAll List<Integer> list) {
List<Integer> sortedOnce = sort(list);
List<Integer> sortedTwice = sort(sortedOnce);
return sortedOnce.equals(sortedTwice);
}
|
Invariant(不変条件)パターン#
操作の前後で保持されるべき性質です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 配列のソート後も長さと要素は保持される
test('ソートしても長さと要素は変わらない', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = [...arr].sort((a, b) => a - b)
// 長さが同じ
if (sorted.length !== arr.length) return false
// 全要素が含まれている
const arrCounts = countElements(arr)
const sortedCounts = countElements(sorted)
return JSON.stringify(arrCounts) === JSON.stringify(sortedCounts)
})
)
})
function countElements(arr) {
return arr.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1
return acc
}, {})
}
|
Symmetry(対称性)パターン#
逆操作や入力の入れ替えで成り立つ性質です。
1
2
3
4
5
6
|
@Property
boolean comparisonIsSymmetric(@ForAll int a, @ForAll int b) {
int cmp1 = Integer.compare(a, b);
int cmp2 = Integer.compare(b, a);
return cmp1 == -cmp2;
}
|
Test Oracle(テストオラクル)パターン#
別の信頼できる実装と比較する方法です。新実装と旧実装、最適化版と単純版などを比較します。
1
2
3
4
5
6
7
8
9
10
|
// 最適化したソートと標準ソートを比較
test('カスタムソートは標準ソートと同じ結果を返す', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const customSorted = customQuickSort([...arr])
const standardSorted = [...arr].sort((a, b) => a - b)
return JSON.stringify(customSorted) === JSON.stringify(standardSorted)
})
)
})
|
実践例: ユーザー登録バリデーション#
実務に近い例として、ユーザー登録のバリデーションをプロパティベーステストで検証してみましょう。
対象となるバリデーション関数#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// validation.js
export function validateUser(user) {
const errors = []
// 名前: 1〜50文字
if (!user.name || user.name.length < 1 || user.name.length > 50) {
errors.push('名前は1〜50文字で入力してください')
}
// メール: 有効な形式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!user.email || !emailRegex.test(user.email)) {
errors.push('有効なメールアドレスを入力してください')
}
// 年齢: 0〜120
if (user.age === undefined || user.age < 0 || user.age > 120) {
errors.push('年齢は0〜120の範囲で入力してください')
}
return {
isValid: errors.length === 0,
errors
}
}
|
プロパティベーステスト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
// validation.property.test.js
import fc from 'fast-check'
import { validateUser } from './validation'
describe('validateUser プロパティテスト', () => {
// 有効なユーザーのArbitrary
const validUserArbitrary = fc.record({
name: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.emailAddress(),
age: fc.integer({ min: 0, max: 120 })
})
// プロパティ1: 有効なユーザーは常に検証を通過する
test('有効なユーザーは常にisValid === true', () => {
fc.assert(
fc.property(validUserArbitrary, (user) => {
const result = validateUser(user)
return result.isValid === true && result.errors.length === 0
})
)
})
// プロパティ2: 無効な名前は必ずエラー
test('名前が空または51文字以上はエラー', () => {
const invalidNameUserArbitrary = fc.record({
name: fc.oneof(
fc.constant(''), // 空文字
fc.string({ minLength: 51, maxLength: 100 }) // 51文字以上
),
email: fc.emailAddress(),
age: fc.integer({ min: 0, max: 120 })
})
fc.assert(
fc.property(invalidNameUserArbitrary, (user) => {
const result = validateUser(user)
return result.isValid === false && result.errors.some((e) => e.includes('名前'))
})
)
})
// プロパティ3: 無効な年齢は必ずエラー
test('年齢が負または121以上はエラー', () => {
const invalidAgeUserArbitrary = fc.record({
name: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.emailAddress(),
age: fc.oneof(
fc.integer({ min: -1000, max: -1 }), // 負の数
fc.integer({ min: 121, max: 1000 }) // 121以上
)
})
fc.assert(
fc.property(invalidAgeUserArbitrary, (user) => {
const result = validateUser(user)
return result.isValid === false && result.errors.some((e) => e.includes('年齢'))
})
)
})
// プロパティ4: バリデーションは冪等
test('同じ入力に対して常に同じ結果を返す(冪等性)', () => {
const anyUserArbitrary = fc.record({
name: fc.option(fc.string({ maxLength: 100 })),
email: fc.option(fc.string()),
age: fc.option(fc.integer({ min: -100, max: 200 }))
})
fc.assert(
fc.property(anyUserArbitrary, (user) => {
const result1 = validateUser(user)
const result2 = validateUser(user)
return (
result1.isValid === result2.isValid &&
JSON.stringify(result1.errors) === JSON.stringify(result2.errors)
)
})
)
})
})
|
プロパティベーステストのベストプラクティス#
適切な試行回数の設定#
デフォルトの試行回数(fast-check: 100, jqwik: 1000)で多くの場合は十分ですが、複雑な状態空間を持つテストでは増やすことを検討してください。
1
2
3
4
5
6
7
|
// fast-check: 試行回数を増やす
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
// ...
}),
{ numRuns: 10000 } // デフォルト100を10000に
)
|
1
2
3
4
5
|
// jqwik: 試行回数を増やす
@Property(tries = 10000) // デフォルト1000を10000に
boolean propertyWithMoreTries(@ForAll List<Integer> list) {
// ...
}
|
filterの使いすぎに注意#
filter()を多用すると、有効なテストデータの生成効率が下がります。可能な限り、最初から条件を満たすArbitraryを構築しましょう。
1
2
3
4
5
|
// 非推奨: filterで絞り込み
const evenArbitrary = fc.integer().filter((n) => n % 2 === 0)
// 推奨: 最初から偶数を生成
const evenArbitrary = fc.integer().map((n) => n * 2)
|
Shrinkingを活かす設計#
複雑なオブジェクトを生成する場合、Shrinkingが効果的に働くよう、プリミティブな部品から組み立てる設計を心がけましょう。
1
2
3
4
5
6
7
8
9
|
// 推奨: Combinators.combineで構築(Shrinkingが効く)
@Provide
Arbitrary<Order> orders() {
return Combinators.combine(
Arbitraries.strings().alpha().ofLength(10), // orderId
Arbitraries.integers().between(1, 100), // quantity
Arbitraries.doubles().between(0.01, 10000.0) // price
).as(Order::new);
}
|
Example-Based TestとProperty-Based Testの併用#
プロパティベーステストは万能ではありません。特定の重要なケースはExample-Based Testで明示的に検証し、一般的な性質はProperty-Based Testで検証する組み合わせが効果的です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
describe('税込価格計算', () => {
// Example-Based: 重要なビジネスケース
test('100円の税込価格は110円', () => {
expect(calculateTaxIncludedPrice(100)).toBe(110)
})
test('0円の税込価格は0円', () => {
expect(calculateTaxIncludedPrice(0)).toBe(0)
})
// Property-Based: 一般的な性質
test('税込価格は常に税抜価格以上', () => {
fc.assert(
fc.property(fc.integer({ min: 0, max: 1000000 }), (price) => {
return calculateTaxIncludedPrice(price) >= price
})
)
})
test('税込価格は税抜価格に比例して増加', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 1000000 }),
fc.integer({ min: 0, max: 1000000 }),
(price1, price2) => {
fc.pre(price1 < price2)
return calculateTaxIncludedPrice(price1) <= calculateTaxIncludedPrice(price2)
}
)
)
})
})
|
まとめ#
プロパティベーステストは、テストデータを自動生成し、コードの普遍的な性質を検証する強力なテスト手法です。
- Property(性質): あらゆる入力で成り立つべき法則を定義する
- Arbitrary/Generator: ランダムなテストデータを自動生成する
- Shrinking: 失敗時に最小の再現ケースを特定する
fast-check(JavaScript/TypeScript)やjqwik(Java)を使うことで、開発者が思いつかなかったエッジケースを含む数百〜数千パターンのテストを自動実行できます。
ただし、プロパティベーステストは万能ではありません。Example-Based Testとの併用により、重要なビジネスケースの明示的な検証と、一般的な性質の網羅的な検証を組み合わせることが、堅牢なテスト戦略の鍵となります。
まずは既存のテストで「可逆性(Round-trip)」や「冪等性(Idempotency)」のパターンを探し、プロパティベーステストを導入してみてください。
参考リンク#