Testing Guide¶
Learn how to test your Tusk applications effectively.
Unit Testing¶
Testing Controllers¶
use PHPUnit\Framework\TestCase;
use Tusk\Web\Http\Request;
use App\Controller\UserController;
class UserControllerTest extends TestCase
{
private UserController $controller;
private UserRepository $mockRepo;
protected function setUp(): void
{
$this->mockRepo = $this->createMock(UserRepository::class);
$this->controller = new UserController($this->mockRepo);
}
public function testListUsers(): void
{
// Arrange
$this->mockRepo
->expects($this->once())
->method('findAll')
->willReturn([
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob'],
]);
$request = new Request('GET', '/users', [], [], '');
// Act
$response = $this->controller->list($request);
// Assert
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody(), true);
$this->assertCount(2, $data);
}
public function testShowUser(): void
{
// Arrange
$this->mockRepo
->expects($this->once())
->method('findById')
->with(1)
->willReturn(['id' => 1, 'name' => 'Alice']);
$request = new Request('GET', '/users/1', [], [], '');
// Act
$response = $this->controller->show($request, 1);
// Assert
$this->assertEquals(200, $response->getStatusCode());
}
}
Testing Repositories¶
use PHPUnit\Framework\TestCase;
use Tusk\Data\Driver\ConnectionInterface;
use App\Repository\UserRepository;
class UserRepositoryTest extends TestCase
{
private UserRepository $repo;
private ConnectionInterface $mockConnection;
protected function setUp(): void
{
$this->mockConnection = $this->createMock(ConnectionInterface::class);
$this->repo = new UserRepository($this->mockConnection);
}
public function testFindById(): void
{
// Arrange
$mockStmt = $this->createMock(\PDOStatement::class);
$mockStmt->expects($this->once())
->method('execute')
->with([1]);
$mockStmt->expects($this->once())
->method('fetch')
->willReturn(['id' => 1, 'name' => 'Alice']);
$this->mockConnection
->expects($this->once())
->method('prepare')
->with('SELECT * FROM users WHERE id = ?')
->willReturn($mockStmt);
// Act
$user = $this->repo->findById(1);
// Assert
$this->assertEquals(1, $user['id']);
$this->assertEquals('Alice', $user['name']);
}
}
Integration Testing¶
Database Testing¶
use PHPUnit\Framework\TestCase;
use Tusk\Data\Driver\Pdo\PdoConnection;
use App\Repository\UserRepository;
class UserRepositoryIntegrationTest extends TestCase
{
private PdoConnection $connection;
private UserRepository $repo;
protected function setUp(): void
{
// Use SQLite in-memory database for testing
$this->connection = new PdoConnection('sqlite::memory:');
// Create schema
$this->connection->exec('
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
');
$this->repo = new UserRepository($this->connection);
}
public function testCreateAndFind(): void
{
// Create user
$id = $this->repo->create([
'name' => 'Alice',
'email' => 'alice@example.com'
]);
// Find user
$user = $this->repo->findById($id);
$this->assertEquals('Alice', $user['name']);
$this->assertEquals('alice@example.com', $user['email']);
}
public function testUpdate(): void
{
$id = $this->repo->create([
'name' => 'Alice',
'email' => 'alice@example.com'
]);
$this->repo->update($id, [
'name' => 'Alice Smith',
'email' => 'alice.smith@example.com'
]);
$user = $this->repo->findById($id);
$this->assertEquals('Alice Smith', $user['name']);
}
}
HTTP Testing¶
Testing Routes¶
use PHPUnit\Framework\TestCase;
use Tusk\Web\Router\Router;
use Tusk\Web\Http\Request;
use Tusk\Core\Container\Container;
class RouterTest extends TestCase
{
private Router $router;
protected function setUp(): void
{
$container = new Container();
$this->router = new Router($container);
$this->router->registerControllers([
\App\Controller\UserController::class,
]);
}
public function testGetUsers(): void
{
$request = new Request('GET', '/users', [], [], '');
$response = $this->router->dispatch($request);
$this->assertEquals(200, $response->getStatusCode());
}
public function testPostUser(): void
{
$request = new Request('POST', '/users',
['Content-Type' => 'application/json'],
[],
json_encode(['name' => 'Alice', 'email' => 'alice@example.com'])
);
$response = $this->router->dispatch($request);
$this->assertEquals(201, $response->getStatusCode());
}
public function test404NotFound(): void
{
$this->expectException(\Tusk\Web\Exception\RouteNotFoundException::class);
$request = new Request('GET', '/nonexistent', [], [], '');
$this->router->dispatch($request);
}
}
Test Fixtures¶
Database Fixtures¶
class DatabaseFixtures
{
public static function loadUsers(ConnectionInterface $connection): void
{
$users = [
['name' => 'Alice', 'email' => 'alice@example.com'],
['name' => 'Bob', 'email' => 'bob@example.com'],
['name' => 'Charlie', 'email' => 'charlie@example.com'],
];
$stmt = $connection->prepare('
INSERT INTO users (name, email) VALUES (?, ?)
');
foreach ($users as $user) {
$stmt->execute([$user['name'], $user['email']]);
}
}
}
// Usage in tests
protected function setUp(): void
{
parent::setUp();
DatabaseFixtures::loadUsers($this->connection);
}
Test Configuration¶
PHPUnit Configuration¶
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
verbose="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>
Running Tests¶
# Run all tests
./vendor/bin/phpunit
# Run specific suite
./vendor/bin/phpunit --testsuite Unit
# Run with coverage
./vendor/bin/phpunit --coverage-html coverage/
Best Practices¶
- Test Isolation - Each test should be independent
- Mock External Dependencies - Don't hit real databases/APIs in unit tests
- Use In-Memory Databases - SQLite for integration tests
- Descriptive Test Names -
testCreateUserWithValidData() - Arrange-Act-Assert - Structure tests clearly
- Test Edge Cases - Not just happy paths
- Keep Tests Fast - Unit tests should run in milliseconds