Friday, December 29, 2023

Unit Testing Scorers

Overview

While working to score hands I mentioned my reason for encapsulating the scoring logic into separate classes was to make it easier to unit test, though I had not created any. First off, shame on me as I call myself a Test-Driven Developer. Secondly, I had a bug in my scoring of Parliament that took me a bit to understand which I'm pretty sure would have been obvious if I had done unit testing. So I'm going to take a bit to create unit tests for my scoring classes.

Setup

In the documentation for Board Game Arena some notes exist for using PHP Unit for automated testing. I struggled to use this to test out the main application because there was a lot of behavior that needed to be mocked out. So while this prompted me to use PHP Unit, I didn't end up needing the autoload.php file since I'm not testing the main application that refers to an external module.

I want these tests to be part of the project and committed to version control. However, I don't want them pushed up to the FTP server where the game is hosted since they will just use up space and not serve any purpose there. Since I'm using the configuration proposed for VS Code I updated the SFTP configuration to ignore the "modules/tests" folder.

Create Tests

The game of Spades scoring is 5 points for every Spade a player wins and -10 for the K of Hearts. The score function expects to receive an array of player IDs and an array of the cards that were won. I came up with the following scenarios:
  • When the player IDs and won cards are empty - which shouldn't happen, but if it does I don't want the code to fail. (Plus, it's a really easy test to setup and make sure things are wired up correctly.)
  • When one player takes all of the cards
  • When one player gets the K of Hearts and the other players share the Spades.

So the test class looks like this.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

require_once "GLTSpadesScorer.class.php";

final class GLTSpadesScorerTest extends TestCase {
    function testScore_emptyValues() {
        $scorer = new GLTSpadesScorer();

        $actual = $scorer->score([], []);

        $expected = [];

        $this->assertEquals($expected, $actual);
    }

    function testScore_onePlayerGetsEverything() {
        $players = [1,2,3,4];
        $won_cards = [
            ['type' => 1, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 8, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 9, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 10, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 11, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 12, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 13, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 14, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 8, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 9, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 10, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 11, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 12, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 13, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 14, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 8, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 9, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 10, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 11, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 12, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 13, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 14, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 8, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 9, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 10, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 11, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 12, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 13, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 14, 'location_arg' => 1],
        ];

        $scorer = new GLTSpadesScorer();

        $actual = $scorer->score($players, $won_cards);

        $expected = [
            1 => 30,
            2 => 0,
            3 => 0,
            4 => 0
        ];

        $this->assertEquals($expected, $actual);
    }

    function testScore_distributedPointsOnePlayerOnlyGetsKingOfHearts() {
        $players = [1,2,3,4];
        $won_cards = [
            ['type' => 1, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 8, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 9, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 10, 'location_arg' => 3],
            ['type' => 1, 'type_arg' => 11, 'location_arg' => 2],
            ['type' => 1, 'type_arg' => 12, 'location_arg' => 1],
            ['type' => 1, 'type_arg' => 13, 'location_arg' => 3],
            ['type' => 1, 'type_arg' => 14, 'location_arg' => 2],
            ['type' => 2, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 2, 'type_arg' => 8, 'location_arg' => 4],
            ['type' => 2, 'type_arg' => 9, 'location_arg' => 4],
            ['type' => 2, 'type_arg' => 10, 'location_arg' => 2],
            ['type' => 2, 'type_arg' => 11, 'location_arg' => 2],
            ['type' => 2, 'type_arg' => 12, 'location_arg' => 2],
            ['type' => 2, 'type_arg' => 13, 'location_arg' => 4],
            ['type' => 2, 'type_arg' => 14, 'location_arg' => 4],
            ['type' => 3, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 8, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 9, 'location_arg' => 2],
            ['type' => 3, 'type_arg' => 10, 'location_arg' => 2],
            ['type' => 3, 'type_arg' => 11, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 12, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 13, 'location_arg' => 1],
            ['type' => 3, 'type_arg' => 14, 'location_arg' => 2],
            ['type' => 4, 'type_arg' => 7, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 8, 'location_arg' => 1],
            ['type' => 4, 'type_arg' => 9, 'location_arg' => 3],
            ['type' => 4, 'type_arg' => 10, 'location_arg' => 3],
            ['type' => 4, 'type_arg' => 11, 'location_arg' => 3],
            ['type' => 4, 'type_arg' => 12, 'location_arg' => 3],
            ['type' => 4, 'type_arg' => 13, 'location_arg' => 3],
            ['type' => 4, 'type_arg' => 14, 'location_arg' => 3],
        ];

        $scorer = new GLTSpadesScorer();

        $actual = $scorer->score($players, $won_cards);

        $expected = [
            1 => 20,
            2 => 10,
            3 => 10,
            4 => -10
        ];

        $this->assertEquals($expected, $actual);
    }
}

The setup for the cards is a little annoying and could probably use some refactoring so that it reads better if nothing else.

Run Tests

From the command line, I go into the modules directory and run the command phpunit tests/. This executes all of the test classes in the "tests" directory.

Conclusion

Well, I feel silly as I thought that was going to take me more time, but since I already had PHP Unit installed I basically just needed to write tests. Now that that's done, I can build out the tests for the other games and explore how I want to deal with Guillotine and Dominoes that each have scoring that isn't related to the cards taken.

No comments: