Testing
The framework is tested with PHPUnit 11 and Mockery. phpunit.xml
defines two suites: Unit (tests/Unit) and Feature
(tests/Feature).
vendor/bin/phpunit
Test Folder Structure
tests/
├── Unit/
│ ├── ActivityManagerChainTest.php # NavigationRequest navigation chain
│ ├── IntentTest.php # Intent object construction/immutability
│ ├── TranslatorTest.php # translation with parameters and fallback
│ └── ViewModelTest.php # ViewModel hydrate/dehydrate
├── Feature/
│ └── BuildSmokeTest.php # overall project health (critical files, manifests)
└── Fixtures/
├── StubContainer.php # minimal PSR-11 container for tests
├── ChainStartActivity.php # fixture Activity that navigates immediately
└── ChainTargetActivity.php # fixture destination Activity
Testing a Single Activity
Since every BotActivity is a plain PHP class (no hidden dependency
on superglobals), unit-testing one is straightforward: instantiate
it, call bindUpdate(), run onCreate() with a custom Intent, and
assert on the getContentView() output:
use Aicrion\Tandroid\Intent\Intent;
use Aicrion\Tandroid\Update\{Update, UpdateType};
use Greeter\ProfileActivity;
final class ProfileActivityTest extends TestCase
{
public function test_it_shows_the_user_id(): void
{
$activity = new ProfileActivity();
$activity->bindUpdate(new Update(updateId: 1, chatId: 1, userId: 99, type: UpdateType::Message));
$activity->onCreate(Intent::action('VIEW_PROFILE'));
$this->assertStringContainsString('99', $activity->getContentView()?->text);
}
}
Testing a Navigation Chain with ActivityManager
For higher-level tests that actually involve
Kernel\ActivityManager (e.g. to verify a NavigationRequest is
actually followed), use Tests\Fixtures\StubContainer — a minimal
PSR-11 container that directly instantiates any zero-argument class
— and Symfony\Component\Cache\Adapter\ArrayAdapter as an in-memory
cache:
$manager = new ActivityManager(
container: new StubContainer(),
intentResolver: new IntentResolver(registry: []),
backStack: new BackStackStore(new ArrayAdapter()),
stateStore: new StateStore(new ArrayAdapter()),
);
$update = new Update(
updateId: 1,
chatId: 555,
userId: 42,
type: UpdateType::CallbackQuery,
callbackData: json_encode(['a' => ChainStartActivity::class, 'p' => []]),
);
$view = $manager->dispatch($update);
$this->assertSame('landed:chain-start', $view->text);
This is exactly the pattern implemented in
tests/Unit/ActivityManagerChainTest.php — see the full example
there.
Testing Repositories / Doctrine-Dependent Code
Since EntityManagerInterface is a regular dependency, mock it with
Mockery for unit tests; for integration tests that actually need a
database, build an in-memory SQLite connection:
$config = ORMSetup::createAttributeMetadataConfiguration([__DIR__ . '/../src/Entity'], isDevMode: true);
$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config);
$entityManager = new EntityManager($connection, $config);
$tool = new \Doctrine\ORM\Tools\SchemaTool($entityManager);
$tool->createSchema($entityManager->getMetadataFactory()->getAllMetadata());
Don't Hit Real Telegram Outbound Calls
Never make real HTTP requests to api.telegram.org in unit tests.
Every Api\*Request class accepts a
Symfony\Contracts\HttpClient\HttpClientInterface in its
constructor — use
Symfony\Component\HttpClient\MockHttpClient:
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$client = new MockHttpClient([new MockResponse(json_encode(['ok' => true, 'result' => []]))]);
$request = new SendMessageRequest($client, 'fake-token');
$request->to(1)->text('Hello')->send();
Guidelines for Writing New Tests
- Put fixture classes (like helper Activities) in
tests/Fixtures/, nottests/Unit/, so PHPUnit doesn't mistakenly try to run them as tests. - For every core architecture change (like modifying
ActivityManager), always add an integration-level test likeActivityManagerChainTest— a regression there means the entire framework's navigation mechanism is broken.