This is a documentation for Board Game Arena: play board games online !
Testing by developer: Difference between revisions
Victoria La (talk | contribs) mNo edit summary |
|||
| (12 intermediate revisions by 2 users not shown) | |||
| Line 25: | Line 25: | ||
===JavaScript=== | ===JavaScript=== | ||
I | I did not try for pure java script, it works for typescript. | ||
You need dev dependency on mocha, jsdom and sinon and some other build tools. | |||
Typescript code lives in src/. | |||
Test code lives in src/tests/. | |||
Tests have their own src/tests/tsconfig.json | |||
File src/tests/setup.ts contains framework and global stubs that tests need. | |||
The config is very verbose so I did not copy it here, if interested check project "dojoless" or bga-dojoless on github (https://github.com/elaskavaia/bga-dojoless). | |||
===PHP=== | ===PHP=== | ||
| Line 33: | Line 39: | ||
Install PHPunit via composer somewhere in home directory, i.e. | Install PHPunit via composer somewhere in home directory, i.e. | ||
cd /home/<yourusername>/php-composer | cd /home/<yourusername>/php-composer | ||
composer require --dev phpunit/phpunit ^ | composer require --dev phpunit/phpunit ^13 | ||
Then add this to your .bashrc | We don't want the phpunit cache to be sync/commited, so add <code>".phpunit.cache"</code> to sftp.json ignore list. You can push the unit tests to the FTP, no need to ignore them. If someone maintains your game in the future, he willl be able to use them to make sure nothing breaks on an update! | ||
If you have a .gitignore, add <code>.phpunit.cache</code> to it. | |||
Then add this to your .bashrc (alias function to be able to test phpunit easily) | |||
phpunit() { | phpunit() { | ||
/home/<yourusername>/php-composer/vendor/bin/phpunit \ | /home/<yourusername>/php-composer/vendor/bin/phpunit \ | ||
| Line 43: | Line 52: | ||
} | } | ||
With that, on a new terminal you | With that, on a new terminal you will be able to run the tests with one of the following commands at the root of your project : | ||
phpunit | phpunit | ||
phpunit tests/BoardManagerTest.php | phpunit tests/BoardManagerTest.php | ||
phpunit --filter testGivesExtraTime | phpunit --filter testGivesExtraTime | ||
====Setup minimal files==== | |||
Create these files: | |||
'''phpunit.xml.dist''' | |||
<pre> | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" | |||
bootstrap="tests/bootstrap.php" | |||
colors="true" | |||
cacheDirectory=".phpunit.cache"> | |||
<testsuites> | |||
<testsuite name="Reversi"> | |||
<directory>tests</directory> | |||
</testsuite> | |||
</testsuites> | |||
</phpunit> | |||
</pre> | |||
(replace Reversi by your project name) | |||
'''tests/bootstrap.php''' | |||
<pre> | <pre> | ||
<?php | <?php | ||
declare(strict_types=1); | |||
require_once __DIR__ . '/stubs/BgaFrameworkStubs.php'; | |||
$autoload = __DIR__ . '/../vendor/autoload.php'; | |||
if (file_exists($autoload)) { | |||
require_once $autoload; | |||
} | |||
spl_autoload_register(static function (string $class): void { | |||
$prefix = 'Bga\\Games\\Reversi\\'; | |||
if (!str_starts_with($class, $prefix)) { | |||
return; | |||
} | |||
$relativeClass = substr($class, strlen($prefix)); | |||
$path = __DIR__ . '/../modules/php/' . str_replace('\\', '/', $relativeClass) . '.php'; | |||
if (file_exists($path)) { | |||
require_once $path; | |||
} | } | ||
}); | }); | ||
</pre> | |||
(replace Reversi by your project name) | |||
'''tests/stubs/BgaFrameworkStubs.php''' | |||
<pre> | |||
<?php | |||
declare(strict_types=1); | |||
require_once __DIR__ . '/stubs/BgaFrameworkStubs.php'; | |||
$autoload = __DIR__ . '/../vendor/autoload.php'; | |||
if (file_exists($autoload)) { | |||
require_once $autoload; | |||
} | |||
spl_autoload_register(static function (string $class): void { | |||
$prefix = 'Bga\\Games\\Reversi\\'; | |||
if (!str_starts_with($class, $prefix)) { | |||
return; | |||
} | |||
$relativeClass = substr($class, strlen($prefix)); | |||
$path = __DIR__ . '/../modules/php/' . str_replace('\\', '/', $relativeClass) . '.php'; | |||
if (file_exists($path)) { | |||
require_once $path; | |||
} | |||
}); | |||
</pre> | </pre> | ||
====Create first tests==== | |||
Example of tests for Reversi: | |||
'''tests/BoardManagerTest.php''' | |||
<pre> | <pre> | ||
<?php declare(strict_types=1); | <?php | ||
declare(strict_types=1); | |||
namespace Tests; | |||
use Bga\Games\Reversi\BoardManager; | |||
use Bga\Games\Reversi\Game; | |||
use PHPUnit\Framework\TestCase; | use PHPUnit\Framework\TestCase; | ||
final class BoardManagerTest extends TestCase | |||
{ | |||
public function testGetPossibleMovesOnInitialBoardForBlackPlayer(): void | |||
{ | |||
$board = $this->createEmptyBoard(8); | |||
$board[4][4] = 2; | |||
$board[5][5] = 2; | |||
$board[4][5] = 1; | |||
$board[5][4] = 1; | |||
$game = $this->createGame(8); | |||
$boardManager = $this->createBoardManager($game, $board); | |||
$moves = $boardManager->getPossibleMoves(1); | |||
self::assertSame( | |||
[ | |||
3 => [4 => true], | |||
4 => [3 => true], | |||
5 => [6 => true], | |||
6 => [5 => true], | |||
], | |||
$moves, | |||
); | |||
} | |||
public function testGetPossibleMovesReturnsEmptyArrayWhenNoMoveExists(): void | |||
{ | |||
$board = $this->createEmptyBoard(4); | |||
for ($x = 1; $x <= 4; $x++) { | |||
for ($y = 1; $y <= 4; $y++) { | |||
$board[$x][$y] = 1; | |||
} | |||
} | |||
class | $game = $this->createGame(4); | ||
$boardManager = $this->createBoardManager($game, $board); | |||
self::assertSame([], $boardManager->getPossibleMoves(2)); | |||
} | |||
private function createGame(int $boardSize): Game | |||
{ | |||
$game = $this->createStub(Game::class); | |||
$game->method('getBoardSize')->willReturn($boardSize); | |||
return $game; | |||
} | |||
private function createBoardManager(Game $game, array $board): BoardManager | |||
{ | |||
return new class ($game, $board) extends BoardManager { | |||
public function __construct(Game $game, private array $board) | |||
{ | |||
parent::__construct($game); | |||
} | |||
public function getBoard(): array | |||
{ | |||
return $this->board; | |||
} | |||
}; | |||
} | |||
private function createEmptyBoard(int $size): array | |||
{ | |||
$board = []; | |||
for ($x = 1; $x <= $size; $x++) { | |||
$board[$x] = []; | |||
for ($y = 1; $y <= $size; $y++) { | |||
$board[$x][$y] = null; | |||
} | |||
} | |||
return $board; | |||
} | } | ||
} | } | ||
</pre> | |||
'''tests/States/NextPlayerTest.php''' | |||
<pre> | |||
<?php | |||
declare(strict_types=1); | |||
namespace Tests\States; | |||
use Bga\Games\Reversi\BoardManager; | |||
use Bga\Games\Reversi\Game; | |||
use Bga\Games\Reversi\States\NextPlayer; | |||
use PHPUnit\Framework\TestCase; | |||
final class NextPlayerTest extends TestCase | |||
{ | |||
public function testReturnsEndScoreWhenNoFreeSquareLeft(): void | |||
{ | |||
$game = $this->createGame(); | |||
$this->attachBoardManager($game, $this->createBoardManagerStub(0, [])); | |||
$state = new NextPlayer($game); | |||
public function | |||
$ | self::assertSame('Bga\\Games\\Reversi\\States\\EndScore', $state->onEnteringState()); | ||
$this-> | } | ||
public function testGivesExtraTimeAndReturnsPlayDiscWhenPlayerCanMove(): void | |||
{ | |||
$game = $this->createGameMock(); | |||
$game->expects(self::once()) | |||
->method('giveExtraTime') | |||
->with(1, null); | |||
$this->attachBoardManager($game, $this->createBoardManagerStub(5, [1 => 4, 2 => 4], [1 => [4 => [3 => true]]])); | |||
$state = new NextPlayer($game); | |||
self::assertSame('Bga\\Games\\Reversi\\States\\PlayDisc', $state->onEnteringState()); | |||
} | |||
private function createGame(int $activePlayerId = 1, int $playerAfterId = 2): Game | |||
{ | |||
$game = $this->createStub(Game::class); | |||
$game->method('activeNextPlayer')->willReturn($activePlayerId); | |||
$game->method('getPlayerAfter')->willReturn($playerAfterId); | |||
return $game; | |||
} | |||
private function createGameMock(int $activePlayerId = 1, int $playerAfterId = 2): Game | |||
{ | |||
$game = $this->getMockBuilder(Game::class) | |||
->disableOriginalConstructor() | |||
->onlyMethods(['activeNextPlayer', 'getPlayerAfter', 'giveExtraTime']) | |||
->getMock(); | |||
$game->method('activeNextPlayer')->willReturn($activePlayerId); | |||
$game->method('getPlayerAfter')->willReturn($playerAfterId); | |||
return $game; | |||
} | |||
private function createBoardManagerStub(int $freeSquares, array $discsByPlayer, array $possibleMovesByPlayer = []): BoardManager | |||
{ | |||
$boardManager = $this->createStub(BoardManager::class); | |||
$boardManager->method('countFreeSquares')->willReturn($freeSquares); | |||
$boardManager->method('getDiscCountsByPlayer')->willReturn($discsByPlayer); | |||
$boardManager->method('getPossibleMoves')->willReturnCallback( | |||
static fn (int $playerId): array => $possibleMovesByPlayer[$playerId] ?? [] | |||
); | |||
return $boardManager; | |||
} | |||
private function attachBoardManager(Game $game, BoardManager $boardManager): void | |||
{ | |||
$game->boardManager = $boardManager; | |||
} | } | ||
} | } | ||
</pre> | |||
====Running the tests==== | |||
Run the tests using | |||
phpunit | |||
If you want to run a single test file | |||
phpunit tests/BoardManagerTest.php | |||
If you want to run a single test | |||
phpunit --filter testGivesExtraTime | |||
<i>If you came here from Reversi Tutorial, go back to it https://en.doc.boardgamearena.com/Tutorial_reversi#Optional:_add_unit_tests</i> | |||
== Play testing on studio == | == Play testing on studio == | ||
Latest revision as of 16:20, 14 March 2026
When you develop a game you obviously have to test it, this page collects the info about testing in one place
Manual Testing on BGA
For manual testing the most important things to know are:
- how to start/stop game in one click
- how to switch between players in one click
- how to save/restore the game state
- how to construct the game state automatically
All of these described here https://en.doc.boardgamearena.com/Tools_and_tips_of_BGA_Studio
There is also HUGE checklist of things you need to test manually which you may not even think about, defined here https://en.doc.boardgamearena.com/Pre-release_checklist
Manual Testing locally
HTML/CSS At this era the file sync is almost instant so you do not save much by doing this locally, however if internet is a challenge - here are some tips https://en.doc.boardgamearena.com/Tools_and_tips_of_BGA_Studio#Speed_up_CSS_development_and_layout
PHP You can run and debug php locally using tip https://en.doc.boardgamearena.com/BGA_Studio_Cookbook#Creating_a_test_class_to_run_PHP_locally
Automated Testing
JavaScript
I did not try for pure java script, it works for typescript. You need dev dependency on mocha, jsdom and sinon and some other build tools. Typescript code lives in src/. Test code lives in src/tests/. Tests have their own src/tests/tsconfig.json File src/tests/setup.ts contains framework and global stubs that tests need. The config is very verbose so I did not copy it here, if interested check project "dojoless" or bga-dojoless on github (https://github.com/elaskavaia/bga-dojoless).
PHP
Install PHPunit
Install PHPunit via composer somewhere in home directory, i.e.
cd /home/<yourusername>/php-composer composer require --dev phpunit/phpunit ^13
We don't want the phpunit cache to be sync/commited, so add ".phpunit.cache" to sftp.json ignore list. You can push the unit tests to the FTP, no need to ignore them. If someone maintains your game in the future, he willl be able to use them to make sure nothing breaks on an update!
If you have a .gitignore, add .phpunit.cache to it.
Then add this to your .bashrc (alias function to be able to test phpunit easily)
phpunit() {
/home/<yourusername>/php-composer/vendor/bin/phpunit \
-c "$PWD/phpunit.xml.dist" \
--bootstrap "$PWD/tests/bootstrap.php" \
"$@"
}
With that, on a new terminal you will be able to run the tests with one of the following commands at the root of your project :
phpunit phpunit tests/BoardManagerTest.php phpunit --filter testGivesExtraTime
Setup minimal files
Create these files:
phpunit.xml.dist
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Reversi">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
(replace Reversi by your project name)
tests/bootstrap.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/stubs/BgaFrameworkStubs.php';
$autoload = __DIR__ . '/../vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
spl_autoload_register(static function (string $class): void {
$prefix = 'Bga\\Games\\Reversi\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$relativeClass = substr($class, strlen($prefix));
$path = __DIR__ . '/../modules/php/' . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($path)) {
require_once $path;
}
});
(replace Reversi by your project name)
tests/stubs/BgaFrameworkStubs.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/stubs/BgaFrameworkStubs.php';
$autoload = __DIR__ . '/../vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
spl_autoload_register(static function (string $class): void {
$prefix = 'Bga\\Games\\Reversi\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$relativeClass = substr($class, strlen($prefix));
$path = __DIR__ . '/../modules/php/' . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($path)) {
require_once $path;
}
});
Create first tests
Example of tests for Reversi:
tests/BoardManagerTest.php
<?php
declare(strict_types=1);
namespace Tests;
use Bga\Games\Reversi\BoardManager;
use Bga\Games\Reversi\Game;
use PHPUnit\Framework\TestCase;
final class BoardManagerTest extends TestCase
{
public function testGetPossibleMovesOnInitialBoardForBlackPlayer(): void
{
$board = $this->createEmptyBoard(8);
$board[4][4] = 2;
$board[5][5] = 2;
$board[4][5] = 1;
$board[5][4] = 1;
$game = $this->createGame(8);
$boardManager = $this->createBoardManager($game, $board);
$moves = $boardManager->getPossibleMoves(1);
self::assertSame(
[
3 => [4 => true],
4 => [3 => true],
5 => [6 => true],
6 => [5 => true],
],
$moves,
);
}
public function testGetPossibleMovesReturnsEmptyArrayWhenNoMoveExists(): void
{
$board = $this->createEmptyBoard(4);
for ($x = 1; $x <= 4; $x++) {
for ($y = 1; $y <= 4; $y++) {
$board[$x][$y] = 1;
}
}
$game = $this->createGame(4);
$boardManager = $this->createBoardManager($game, $board);
self::assertSame([], $boardManager->getPossibleMoves(2));
}
private function createGame(int $boardSize): Game
{
$game = $this->createStub(Game::class);
$game->method('getBoardSize')->willReturn($boardSize);
return $game;
}
private function createBoardManager(Game $game, array $board): BoardManager
{
return new class ($game, $board) extends BoardManager {
public function __construct(Game $game, private array $board)
{
parent::__construct($game);
}
public function getBoard(): array
{
return $this->board;
}
};
}
private function createEmptyBoard(int $size): array
{
$board = [];
for ($x = 1; $x <= $size; $x++) {
$board[$x] = [];
for ($y = 1; $y <= $size; $y++) {
$board[$x][$y] = null;
}
}
return $board;
}
}
tests/States/NextPlayerTest.php
<?php
declare(strict_types=1);
namespace Tests\States;
use Bga\Games\Reversi\BoardManager;
use Bga\Games\Reversi\Game;
use Bga\Games\Reversi\States\NextPlayer;
use PHPUnit\Framework\TestCase;
final class NextPlayerTest extends TestCase
{
public function testReturnsEndScoreWhenNoFreeSquareLeft(): void
{
$game = $this->createGame();
$this->attachBoardManager($game, $this->createBoardManagerStub(0, []));
$state = new NextPlayer($game);
self::assertSame('Bga\\Games\\Reversi\\States\\EndScore', $state->onEnteringState());
}
public function testGivesExtraTimeAndReturnsPlayDiscWhenPlayerCanMove(): void
{
$game = $this->createGameMock();
$game->expects(self::once())
->method('giveExtraTime')
->with(1, null);
$this->attachBoardManager($game, $this->createBoardManagerStub(5, [1 => 4, 2 => 4], [1 => [4 => [3 => true]]]));
$state = new NextPlayer($game);
self::assertSame('Bga\\Games\\Reversi\\States\\PlayDisc', $state->onEnteringState());
}
private function createGame(int $activePlayerId = 1, int $playerAfterId = 2): Game
{
$game = $this->createStub(Game::class);
$game->method('activeNextPlayer')->willReturn($activePlayerId);
$game->method('getPlayerAfter')->willReturn($playerAfterId);
return $game;
}
private function createGameMock(int $activePlayerId = 1, int $playerAfterId = 2): Game
{
$game = $this->getMockBuilder(Game::class)
->disableOriginalConstructor()
->onlyMethods(['activeNextPlayer', 'getPlayerAfter', 'giveExtraTime'])
->getMock();
$game->method('activeNextPlayer')->willReturn($activePlayerId);
$game->method('getPlayerAfter')->willReturn($playerAfterId);
return $game;
}
private function createBoardManagerStub(int $freeSquares, array $discsByPlayer, array $possibleMovesByPlayer = []): BoardManager
{
$boardManager = $this->createStub(BoardManager::class);
$boardManager->method('countFreeSquares')->willReturn($freeSquares);
$boardManager->method('getDiscCountsByPlayer')->willReturn($discsByPlayer);
$boardManager->method('getPossibleMoves')->willReturnCallback(
static fn (int $playerId): array => $possibleMovesByPlayer[$playerId] ?? []
);
return $boardManager;
}
private function attachBoardManager(Game $game, BoardManager $boardManager): void
{
$game->boardManager = $boardManager;
}
}
Running the tests
Run the tests using
phpunit
If you want to run a single test file
phpunit tests/BoardManagerTest.php
If you want to run a single test
phpunit --filter testGivesExtraTime
If you came here from Reversi Tutorial, go back to it https://en.doc.boardgamearena.com/Tutorial_reversi#Optional:_add_unit_tests
Play testing on studio
To play test on studio you can use your test accounts dev0... dev9. You can give some of these account to other people just make sure you change the password. You should not encourage other people who are not developers to create studio account, this is against bga policy.