PHPUnit から Go へ: Go 開発者のためのデータ駆動型単体テスト

Patricia Arquette
リリース: 2024-11-12 19:21:02
オリジナル
240 人が閲覧しました

From PHPUnit to Go: Data-Driven Unit Testing for Go Developers

この投稿では、PHP 単体テストの考え方、特に PHPUnit フレームワークのデータ プロバイダー アプローチを Go に導入する方法を検討します。経験豊富な PHP 開発者であれば、生の配列でテスト データを個別に収集し、このデータをテスト関数に供給するデータ プロバイダー モデルに精通しているはずです。このアプローチにより、単体テストがよりクリーンで保守しやすくなり、オープン/クローズなどの原則に準拠します。

なぜデータプロバイダーアプローチを採用するのか?

Go で単体テストを構造化するためにデータ プロバイダーのアプローチを使用すると、次のようないくつかの利点があります。

可読性と拡張性の強化: テストが視覚的に整理され、上部に各テスト シナリオを表す明確に分離された配列が表示されます。各配列のキーはシナリオを記述し、そのコンテンツにはそのシナリオをテストするためのデータが保持されます。この構造により、ファイルは快適に作業でき、拡張も簡単になります。

懸念事項の分離: データ プロバイダー モデルはデータとテスト ロジックを分離し、その結果、時間が経ってもほとんど変化しない、軽量で分離された関数が得られます。新しいシナリオを追加するには、プロバイダーにさらにデータを追加するだけで済みます。これにより、テスト関数は拡張に対してオープンのままにし、変更に対してはクローズされます。これは、テストにおけるオープン/クローズ原則の実際的な適用です。

一部のプロジェクトでは、別の JSON ファイルをデータ ソースとして使用し、手動で構築してプロバイダーに供給し、プロバイダーがテスト関数にデータを供給することを正当化するほど高密度のシナリオも見てきました。

データプロバイダーの使用が非常に推奨されるのはどのような場合ですか?

さまざまなデータを含む多数のテスト ケースがある場合は、データ プロバイダーの使用が特に推奨されます。各テスト ケースは概念的には似ていますが、入力と予想される出力が異なるだけです。

単一のテスト関数内でデータとロジックが混在すると、開発者エクスペリエンス (DX) が低下する可能性があります。多くの場合、次のような結果になります。

冗長オーバーロード: わずかなデータ変化を伴うステートメントを繰り返す冗長コードにより、追加の利点のない冗長なコードベースが生成されます。

明瞭さの低下: 実際のテスト データを周囲のコードから分離しようとすると、テスト関数をスキャンするのが面倒になりますが、データ プロバイダーのアプローチにより自然に軽減されます。

なるほど、それではデータプロバイダーとは何でしょうか?

PHPUnit の DataProvider パターン。基本的にプロバイダー関数は、暗黙的なループで消費されるさまざまなデータセットをテスト関数に提供します。これにより、コア テスト機能のロジックを変更することなく、テスト シナリオの追加または変更が容易になり、DRY (Don't Reply Yourself) 原則が保証され、オープン/クローズド原則にも準拠します。

データプロバイダーなしで問題を解決するには?

冗長性、コードの重複、メンテナンスの課題の欠点を説明するために、データ プロバイダーの助けを借りないバブル ソート関数の単体テストの例のスニペットを次に示します。

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class BubbleSortTest extends TestCase
{
    public function testBubbleSortEmptyArray()
    {
        $this->assertSame([], BubbleSort([]));
    }

    public function testBubbleSortOneElement()
    {
        $this->assertSame([0], BubbleSort([0]));
    }

    public function testBubbleSortTwoElementsSorted()
    {
        $this->assertSame([5, 144], BubbleSort([5, 144]));
    }

    public function testBubbleSortTwoElementsUnsorted()
    {
        $this->assertSame([-7, 10], BubbleSort([10, -7]));
    }

    public function testBubbleSortMultipleElements()
    {
        $this->assertSame([1, 2, 3, 4], BubbleSort([1, 3, 4, 2]));
    }

    // And so on for each test case, could be 30 cases for example.

    public function testBubbleSortDescendingOrder()
    {
        $this->assertSame([1, 2, 3, 4, 5], BubbleSort([5, 4, 3, 2, 1]));
    }

    public function testBubbleSortBoundaryValues()
    {
        $this->assertSame([-2147483647, 2147483648], BubbleSort([2147483648, -2147483647]));
    }
}

ログイン後にコピー

上記のコードに問題はありますか?確かに:

冗長性: 各テスト ケースには個別のメソッドが必要であり、その結果、コードベースが大規模で反復的になります。

重複: テスト ロジックは各メソッドで繰り返され、入力と予想される出力によってのみ異なります。

オープン/クローズド違反: 新しいテスト ケースを追加するには、さらにメソッドを作成してテスト クラス構造を変更する必要があります。

データプロバイダーの問題を解決します!

これは、データプロバイダーを使用するためにリファクタリングされた同じテストスイートです

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class BubbleSortTest extends TestCase
{
    /**
     * Provides test data for bubble sort algorithm.
     *
     * @return array<string, array>
     */
    public function bubbleSortDataProvider(): array
    {
        return [
            'empty' => [[], []],
            'oneElement' => [[0], [0]],
            'twoElementsSorted' => [[5, 144], [5, 144]],
            'twoElementsUnsorted' => [[10, -7], [-7, 10]],
            'moreThanOneElement' => [[1, 3, 4, 2], [1, 2, 3, 4]],
            'moreThanOneElementWithRepetition' => [[1, 4, 4, 2], [1, 2, 4, 4]],
            'moreThanOneElement2' => [[7, 7, 1, 0, 99, -5, 10], [-5, 0, 1, 7, 7, 10, 99]],
            'sameElement' => [[1, 1, 1, 1], [1, 1, 1, 1]],
            'negativeNumbers' => [[-5, -2, -10, -1, -3], [-10, -5, -3, -2, -1]],
            'descendingOrder' => [[5, 4, 3, 2, 1], [1, 2, 3, 4, 5]],
            'randomOrder' => [[9, 2, 7, 4, 1, 6, 3, 8, 5], [1, 2, 3, 4, 5, 6, 7, 8, 9]],
            'duplicateElements' => [[2, 2, 1, 1, 3, 3, 4, 4], [1, 1, 2, 2, 3, 3, 4, 4]],
            'largeArray' => [[-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524], [-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524]],
            'singleNegativeElement' => [[-7], [-7]],
            'arrayWithZeroes' => [[0, -2, 0, 3, 0], [-2, 0, 0, 0, 3]],
            'ascendingOrder' => [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5]],
            'descendingOrderWithDuplicates' => [[5, 5, 4, 3, 3, 2, 1], [1, 2, 3, 3, 4, 5, 5]],
            'boundaryValues' => [[2147483648, -2147483647], [-2147483647, 2147483648]],
            'mixedSignNumbers' => [[-1, 0, 1, -2, 2], [-2, -1, 0, 1, 2]],
        ];
    }

    /**
     * @dataProvider bubbleSortDataProvider
     *
     * @param array<int> $input
     * @param array<int> $expected
     */
    public function testBubbleSort(array $input, array $expected)
    {
        $this->assertSame($expected, BubbleSort($input));
    }
}

ログイン後にコピー

データプロバイダーを使用する利点はありますか?そうそう:

簡潔性: すべてのテスト データが 1 つのメソッドに集中され、シナリオごとに複数の関数を使用する必要がなくなります。

可読性の向上: 各テスト ケースはよく整理されており、シナリオごとに説明的なキーが付いています。

オープン/クローズド原則: コアのテスト ロジックを変更せずに、新しいケースをデータ プロバイダーに追加できます。

改善された DX (開発者エクスペリエンス): テスト構造はすっきりしていて、目に魅力的で、怠惰な開発者でも拡張、デバッグ、更新する意欲を高めます。

データプロバイダーの活用

  • Go には PHPUnit のようなネイティブ データ プロバイダー モデルがないため、別のアプローチを使用する必要があります。いくつかのレベルの複雑さを持つ多くの実装が存在する可能性があります。以下は、Go ランドでデータ プロバイダーをシミュレートする候補となる平均的な実装です。
package sort

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

type TestData struct {
    ArrayList    map[string][]int
    ExpectedList map[string][]int
}

const (
    maxInt32 = int32(^uint32(0) >> 1)
    minInt32 = -maxInt32 - 1
)

var testData = &TestData{
    ArrayList: map[string][]int{
        "empty":                            {},
        "oneElement":                       {0},
        "twoElementsSorted":                {5, 144},
        "twoElementsUnsorted":              {10, -7},
        "moreThanOneElement":               {1, 3, 4, 2},
        "moreThanOneElementWithRepetition": {1, 4, 4, 2},
        "moreThanOneElement2":              {7, 7, 1, 0, 99, -5, 10},
        "sameElement":                      {1, 1, 1, 1},
        "negativeNumbers":                  {-5, -2, -10, -1, -3},
        "descendingOrder":                  {5, 4, 3, 2, 1},
        "randomOrder":                      {9, 2, 7, 4, 1, 6, 3, 8, 5},
        "duplicateElements":                {2, 2, 1, 1, 3, 3, 4, 4},
        "largeArray":                       {-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524},
        "singleNegativeElement":            {-7},
        "arrayWithZeroes":                  {0, -2, 0, 3, 0},
        "ascendingOrder":                   {1, 2, 3, 4, 5},
        "descendingOrderWithDuplicates":    {5, 5, 4, 3, 3, 2, 1},
        "boundaryValues":                   {2147483648, -2147483647},
        "mixedSignNumbers":                 {-1, 0, 1, -2, 2},
    },
    ExpectedList: map[string][]int{
        "empty":                            {},
        "oneElement":                       {0},
        "twoElementsSorted":                {5, 144},
        "twoElementsUnsorted":              {-7, 10},
        "moreThanOneElement":               {1, 2, 3, 4},
        "moreThanOneElementWithRepetition": {1, 2, 4, 4},
        "moreThanOneElement2":              {-5, 0, 1, 7, 7, 10, 99},
        "sameElement":                      {1, 1, 1, 1},
        "negativeNumbers":                  {-10, -5, -3, -2, -1},
        "descendingOrder":                  {1, 2, 3, 4, 5},
        "randomOrder":                      {1, 2, 3, 4, 5, 6, 7, 8, 9},
        "duplicateElements":                {1, 1, 2, 2, 3, 3, 4, 4},
        "largeArray":                       {-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524},
        "singleNegativeElement":            {-7},
        "arrayWithZeroes":                  {-2, 0, 0, 0, 3},
        "ascendingOrder":                   {1, 2, 3, 4, 5},
        "descendingOrderWithDuplicates":    {1, 2, 3, 3, 4, 5, 5},
        "boundaryValues":                   {-2147483647, 2147483648},
        "mixedSignNumbers":                 {-2, -1, 0, 1, 2},
    },
}

func TestBubble(t *testing.T) {

    for testCase, array := range testData.ArrayList {
        t.Run(testCase, func(t *testing.T) {
            actual := Bubble(array)
            assert.ElementsMatch(t, actual, testData.ExpectedList[testCase])
        })

    }
}
ログイン後にコピー
  • 基本的に 2 つのマップ/リストを定義します。1 つは入力データ用、もう 1 つは期待されるデータ用です。両側の各ケース シナリオが両側の同じマップ キーを通じて参照されることを保証します。
  • テストの実行は、準備された入力/期待リストを反復する単純な関数のループの問題です。
  • 一部のワンタイム定型文を除き、テストの変更はデータ側でのみ行われるべきであり、ほとんどの場合、テストを実行する関数のロジックを変更する変更はありません。これにより、上で説明した次の目標が達成されます。生データの準備に至るまでのテスト作業。

ボーナス: このブログ投稿で紹介されているロジックを実装する Github リポジトリは、https://github.com/MedUnes/dsa-go にあります。これまでのところ、これらのテストを実行し、超有名な緑色のバッジを表示する Github アクションも含まれています ;)

次の有益な投稿でお会いしましょう!

以上がPHPUnit から Go へ: Go 開発者のためのデータ駆動型単体テストの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート