「ソフトウェアテスト技法練習帳」40本ノック(1)/RPGゲーム

きっかけ

現在テストを書く業務がないため、練習・実践できるものを探していた矢先
こちらを見つけ、実際にプログラムを書いてみようと思いました。
全てを記事にするのは大変なので、いくつかピックアップした問題について記事を書けたらと思います。
※コードは全てGithubにあげる予定です。
指摘・アドバイス大歓迎です!!!

問題1:勇者に装備できる武器の組み合わせ

仕様

・武器には「片手持ち」と「両手持ち」の武器がある
・勇者は常に盾を装備している
・盾は「片手持ち」である
・盾は1つまで装備できる
・盾を装備している状態で「両手持ち」の武器を装備しようとすると「装備できません」と出力する

武器 攻撃力 タイプ 強化
けやきのぼう 5 片手持ち 不可能
ニンジンの短剣 20 片手持ち 不可能
きこりの大斧 25 両手持ち 不可能
龍の槍 50 両手持ち 可能
草薙の剣 90 片手持ち 可能

テストコード

rpgTest.php

<?php

use App\Rpg\Hero;
use App\Rpg\Weapons\LumberjackAxe;
use App\Rpg\Weapons\Shield;
use App\Rpg\Weapons\ZelkovaRod;
use PHPUnit\Framework\TestCase;

class rpgTest extends TestCase{

    protected function setUp() :void {
        
    }

    /**
     * @test
     * @dataProvider 盾を装備している時の装備可能テスト用データ
     */
    public function 盾を装備している時の装備可能テスト(
        Array $weapons,
        Shield $shield,
        String $expectedMessage,
    ) {
        $message = '';

        try {
            new Hero(
                $weapons,
                $shield
            );
        }catch(\Exception $e) {
            $message = $e->getMessage();
        }

        $this->assertEquals($expectedMessage, $message);
    }

    public static function 盾を装備している時の装備可能テスト用データ() {
        return [
            '武器を装備しなくても良い' => [
                [],
                new Shield(),
                '',
            ],
            '片手持ち武器は装備することができる' => [
                [new ZelkovaRod(enhanced: false),],
                new Shield(),
                '',
            ],
            '両手持ちは装備することができない' => [
                [new LumberjackAxe(enhanced: false),],
                new Shield(),
                '装備できません',
            ],
            '武器でないものは装備できない' => [
                [new Shield(),],
                new Shield(),
                '武器ではないものを装備しようとしています。',
            ],
        ];
    }
}

武器オブジェクトにはWeaponインターフェースを実装させることで
勇者オブジェクトに武器の配列を渡した時に、内部でバリデーションができるようにしています。

勇者クラス

Hero.php

<?php
namespace App\Rpg;

use App\Rpg\Weapons\Shield;

class Hero {

    const handNum = 2;

    private Array $weapons;

    private ?Shield $shield;

    public function __construct(
        Array $weapons,
        ?Shield $shield,
    ) {
        $useHandNum = 0;

        foreach($weapons as $weapon) {
            $interfaces = class_implements($weapon);
            $include = false;
            foreach ($interfaces as $key => $value) {
                if (str_contains($value, 'WeaponInterface')) {
                    $include = true;
                    break;
                }
            }
            if (!$include) {
                throw new \Exception('武器ではないものを装備しようとしています。');
            }

            $useHandNum+=$weapon->getHandedType()->value;
        }

        if (!is_null($shield)) {
            $useHandNum++;
        }

        if (self::handNum < $useHandNum) {
            throw new \Exception('装備できません');
        }

        $this->weapons = $weapons;
    }
}

答え合わせ

上のテストコードはすでに修正済みで
実際には全ての武器オブジェクトの数だけテストデータを渡していました。
同値分割法的には出力結果が異なるグループごとに1つのテストデータで十分なため
解答ではテストデータは2つで十分でした。
また、解答とは別に個人的に
・何も装備しなかった場合
・盾を武器として装備させようとした場合
のテストデータを書いていました。

問題2:武器の強化可能判定

仕様

・武器には強化できるものとできない武器がある
・強化は一度しかできない
・強化可能武器を強化すると攻撃力が10増え「武器の攻撃力が上がりました」と出力する
・強化不可能な武器を強化しようとすると「武器の強化に失敗しました」と出力する
・強化済みの武器を強化しようとすると「これ以上この武器の強化はできません」と出力する

テストコード

rpgTest.php

<?php

use App\Rpg\Weapons\DragonSpear;
use App\Rpg\Weapons\Interface\WeaponInterface;
use App\Rpg\Weapons\ZelkovaRod;
use PHPUnit\Framework\TestCase;

class rpgTest extends TestCase{

    protected function setUp() :void {
        
    }

    /**
     * @test
     * @dataProvider 強化可能テスト用データ
     */
    public function 強化可能テスト(
        WeaponInterface $weapon,
        String $expectedMessage,
        int $expectedAttack,
    ) {
        $message = null;
        $attack = $weapon->getAttackPower();
        try {
            $enhancedWeapon = $weapon->enhance();
            $message = '武器の攻撃力が上がりました';
            $attack = $enhancedWeapon->getAttackPower();
        } catch(\Exception $e) {
            $message = $e->getMessage();
        }

        $this->assertEquals($expectedMessage, $message);
        $this->assertEquals($expectedAttack, $attack);
    }

    public static function 強化可能テスト用データ() {
        return [
            '強化可能な武器を強化する' => [
                new DragonSpear(enhanced: false),
                '武器の攻撃力が上がりました',
                DragonSpear::BaseAttack+10,
            ],
            '強化済みの武器を強化する' => [
                new DragonSpear(enhanced: true),
                'これ以上この武器の強化はできません',
                DragonSpear::BaseAttack+10,
            ],
            '強化不可能な武器を強化する' => [
                new ZelkovaRod(enhanced: false),
                '武器の強化に失敗しました',
                ZelkovaRod::BaseAttack,
            ],
        ];
    }
}

答え合わせ

この問題については解答と同じ数のテストパターンを網羅できていました。