Talon¶
Overview¶
Talon is the Phalcon test harness. It is the part of Phalcon that catches the bugs. Talon bootstraps a Phalcon test environment and fronts PHPUnit, so any Phalcon project can write unit, database, functional, and browser tests with minimal boilerplate.
Talon provides three things:
- Traits — the framework-neutral core. Each trait carries a group of helpers (reflection, filesystem, database, functional, browser, services).
- PHPUnit base classes — ready-to-extend test cases that compose the traits for each kind of test.
- A command-line runner —
vendor/bin/talon, which runs PHPUnit once per mapped suite.
Talon runs on both Phalcon distributions and uses whichever is present:
- Phalcon v5 — the
ext-phalconC extension. - Phalcon v6 — the
phalcon/phalconPHP package.
The same test suite runs against either one. Talon is used across the Phalcon projects, including cphalcon, the sample applications, and the PHP framework itself. To see it driving the cphalcon suites, see the Testing environment guide.
Requirements¶
- PHP 8.1 or later.
- Phalcon, either the v5 C extension or the v6
phalcon/phalconpackage. See the installation page for the extension. symfony/browser-kitandsymfony/dom-crawlerare pulled in as dependencies and power the browser tests.
Installation¶
Install Talon as a development dependency:
Bootstrapping Your Tests¶
Point PHPUnit at a bootstrap file that boots Talon. The one-liner form reads configuration from the environment:
<?php
// tests/bootstrap.php
require __DIR__ . '/../vendor/autoload.php';
use Phalcon\Talon\Settings;
use Phalcon\Talon\Talon;
Talon::boot(Settings::fromEnv());
When you need setup hooks (for example, raising the memory limit or creating output directories), use the bootstrap Runner. It runs your callbacks before or after each bootstrap Stage — Stage::Settings, Stage::Environment, and Stage::Directories:
<?php
use Phalcon\Talon\Bootstrap\Runner;
use Phalcon\Talon\Bootstrap\Stage;
use Phalcon\Talon\Settings;
Runner::for(Settings::fromArray(['root' => __DIR__ . '/..']))
->before(Stage::Environment, fn () => ini_set('memory_limit', '512M'))
->after(Stage::Directories, fn ($settings) => mkdir($settings->outputPath('screens'), 0777, true))
->boot();
The Command-Line Runner¶
vendor/bin/talon runs PHPUnit once per mapped suite:
vendor/bin/talon run # default suite (unit)
vendor/bin/talon run mysql
vendor/bin/talon run mysql pgsql
vendor/bin/talon run all # every mapped suite, sequentially
vendor/bin/talon suites # list mapped suites
Each suite runs as its own subprocess, so per-suite extensions and environment variables take effect. A single suite forwards its exit code verbatim. Multiple suites print a per-suite summary and exit with the highest code.
Suite discovery¶
With zero configuration, suites are discovered from phpunit*.xml files in the project root and in resources/. phpunit.xml.dist becomes the unit suite (the default), and phpunit.mysql.xml becomes the mysql suite.
Projects that need PHP ini flags or environment variables declare a talon.php at the project root:
<?php
return [
'php' => ['extension=ext/modules/phalcon.so'], // global ini flags, optional
'suites' => [
'unit' => ['config' => 'resources/phpunit.xml.dist'],
'mysql' => ['config' => 'resources/phpunit.mysql.xml'],
'pgsql' => ['config' => 'resources/phpunit.pgsql.xml'],
'sqlite' => ['config' => 'resources/phpunit.sqlite.xml'],
],
'default' => 'unit',
];
Per-suite keys:
| Key | Required | Description |
|---|---|---|
config | Yes | Path to the PHPUnit configuration for the suite |
php | No | Extra PHP ini flags, merged over the global php |
env | No | Extra environment variables, merged over global env |
args | No | Default PHPUnit arguments for the suite |
Forwarding arguments to PHPUnit¶
Options are forwarded to PHPUnit starting at the first option Talon does not recognize itself. Everything after -- is always forwarded verbatim:
Test Case Base Classes¶
Each base class composes the relevant traits for one kind of test. They live in the Phalcon\Talon\PHPUnit namespace:
| Base class | For | Highlights |
|---|---|---|
AbstractUnitTestCase | Unit tests | Reflection and filesystem helpers |
AbstractDatabaseTestCase | Database tests | assertInDatabase(); driver from the driver env |
AbstractFunctionalTestCase | Functional tests | Dispatch a route through your application and assert the result |
AbstractBrowserTestCase | Multi-request flows | In-process browser; cookies and session preserved across requests |
AbstractServicesTestCase | Redis / Memcached | Cache helpers; auto-skip when the backend is unreachable |
Unit tests¶
<?php
use Phalcon\Talon\PHPUnit\AbstractUnitTestCase;
final class CalculatorTest extends AbstractUnitTestCase
{
public function testInternal(): void
{
$this->assertSame(5, $this->callProtectedMethod(new Calculator(), 'add', 2, 3));
}
}
AbstractUnitTestCase provides callProtectedMethod(), getProtectedProperty(), setProtectedProperty(), invokeMethod(), getNewFileName(), safeDeleteFile(), safeDeleteDirectory(), assertFileContentsContains(), checkExtensionIsLoaded(), and checkPhalconAvailable().
Database tests¶
<?php
use Phalcon\Talon\PHPUnit\AbstractDatabaseTestCase;
final class UserTest extends AbstractDatabaseTestCase
{
public function testSeeded(): void
{
$this->assertInDatabase('users', ['email' => '[email protected]']);
}
}
The driver comes from the driver environment variable (sqlite, mysql, or pgsql). Credentials come from Settings, read from environment variables by default.
Functional tests¶
Talon never owns your container. Hand it your configured application through an appFactory():
<?php
use Phalcon\Talon\PHPUnit\AbstractFunctionalTestCase;
final class HomeTest extends AbstractFunctionalTestCase
{
protected function appFactory(): callable
{
// returns a configured Application or Micro
return fn () => require __DIR__ . '/../app/bootstrap.php';
}
public function testHome(): void
{
$this->dispatch('/');
$this->assertController('index');
$this->assertResponseContentContains('Welcome');
}
}
Browser tests¶
For multi-request flows — login, forms, redirects — AbstractBrowserTestCase drives your application in-process, with no web server, through a symfony/browser-kit bridge. Cookies and the session are kept across requests, and redirects are followed automatically:
<?php
use Phalcon\Talon\PHPUnit\AbstractBrowserTestCase;
final class LoginTest extends AbstractBrowserTestCase
{
protected function appFactory(): callable
{
return fn () => require __DIR__ . '/../app/bootstrap.php';
}
public function testLogin(): void
{
$this->visitPage('/session/login');
$this->fillField('email', '[email protected]');
$this->fillField('password', 'password1');
$this->pressButton('Log In');
$this->assertPageContainsText('Search users');
}
}
The browser verbs are visitPage, fillField, selectOption, clickLink, pressButton, and getCookie/setCookie. The assertions are assertPageContainsText and assertPageMissingText. Browser tests require symfony/browser-kit and symfony/dom-crawler, which Talon installs.
Service tests¶
<?php
use Phalcon\Talon\PHPUnit\AbstractServicesTestCase;
final class CacheTest extends AbstractServicesTestCase
{
public function testRedis(): void
{
$this->setRedisKey('key', 'value');
$this->assertSame('value', $this->getRedisKey('key'));
}
}
Service tests skip automatically when the backend (Redis or Memcached) is unreachable, so the suite stays green on a host without those services.
Mocking a resultset¶
To assert against model logic without a database, mock a resultset with ResultSetTrait:
<?php
use Phalcon\Talon\Traits\ResultSetTrait;
use PHPUnit\Framework\TestCase;
final class ReportTest extends TestCase
{
use ResultSetTrait;
public function testReport(): void
{
$resultset = $this->mockResultSet([$modelA, $modelB]);
$this->assertCount(2, $resultset);
}
}
Traits¶
The traits are the core public API. Compose them directly when you do not want the base classes. They live in the Phalcon\Talon\Traits namespace:
| Trait | Provides |
|---|---|
ReflectionTrait | callProtectedMethod, getProtectedProperty, setProtectedProperty, invokeMethod |
FileSystemTrait | getNewFileName, safeDeleteFile, safeDeleteDirectory, assertFileContentsContains, assertFileContentsEqual |
DatabaseTrait | assertInDatabase, assertNotInDatabase, getConnection |
FunctionalTrait | dispatch, getContent |
FunctionalAssertionsTrait | assertController, assertAction, assertResponseCode, assertRedirectTo, assertResponseContentContains, assertHeader, assertDispatchIsForwarded |
BrowserTrait | visitPage, fillField, selectOption, clickLink, pressButton, getCookie, setCookie |
BrowserAssertionsTrait | assertPageContainsText, assertPageMissingText |
ServicesTrait | setRedisKey, getRedisKey, hasRedisKey, sendRedisCommand, setMemcachedKey, getMemcachedKey, clearMemcached |
ResultSetTrait | mockResultSet |
Configuration¶
Configuration is held in a Settings object. The one-liner bootstrap uses Settings::fromEnv(), which reads environment variables. To configure Talon in code, pass Settings::fromArray() to Talon::boot(), or override getSettings() in a project base class:
<?php
use Phalcon\Talon\Settings;
use Phalcon\Talon\Talon;
Talon::boot(
Settings::fromArray(
[
'root' => dirname(__DIR__),
'db' => [
'mysql' => [
'host' => '127.0.0.1',
'port' => 3306,
'dbname' => 'app',
'username' => 'root',
'password' => '',
],
'sqlite' => [
'dbname' => ':memory:',
],
],
]
)
);
Beyond PHPUnit¶
The traits carry no PHPUnit base-class requirement for their non-assertion helpers, so Pest (through uses(...)) and other runners can consume them too. Pest and Codeception adapters are planned for a future release.