はじめに

「テストケースは書いたのに、本番で予想外の入力でバグが発生した」という経験はないでしょうか。手動で考えたテストケースには限界があり、開発者が想定しなかったエッジケースを見落としがちです。

プロパティベーステスト(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:

1
pnpm add -D fast-check

基本的な使い方

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)」のパターンを探し、プロパティベーステストを導入してみてください。

参考リンク