Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bridge/vcr/VCR.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* @param RecordMode|null $mode Record mode for this test; `null` inherits php-vcr's global default
* ({@see RecordMode::NewEpisodes}).
* @param list<Matcher> $match Request matchers for this test; an empty list inherits php-vcr's
* default (method + URL).
* default (every available matcher).
*/
public function __construct(
public string $name,
Expand Down
1 change: 1 addition & 0 deletions testo.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
name: 'sandbox',
location: new FinderConfig(
include: ['tests/Sandbox'],
exclude: ['tests/Sandbox/Sample'],
),
),
],
Expand Down
72 changes: 72 additions & 0 deletions tests/Sandbox/Sample/01_OrderAssertions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

/**
* Slide 1. Checking a domain object: typed assertion chains vs a wall of `$this->assert*`.
*/

namespace Sample\PhpUnit {

use App\Checkout\Order;
use PHPUnit\Framework\TestCase;

final class OrderTest extends TestCase
{
public function testPlacesOrder(): void
{
$order = Order::place(customerId: 42, items: ['sku-1' => 2, 'sku-2' => 1]);

$this->assertIsString($order->number);
$this->assertStringStartsWith('ORD-', $order->number);
$this->assertStringNotContainsString(' ', $order->number);

$this->assertIsInt($order->total);
$this->assertGreaterThan(0, $order->total);
$this->assertLessThanOrEqual(1_000_000, $order->total);

$this->assertIsArray($order->lines);
$this->assertCount(2, $order->lines);
$this->assertArrayHasKey('sku-1', $order->lines);
$this->assertArrayHasKey('sku-2', $order->lines);
$this->assertArrayNotHasKey('sku-3', $order->lines);
$this->assertContainsOnlyInstancesOf(Order\Line::class, $order->lines);

$this->assertSame(42, $order->customerId);
$this->assertNull($order->paidAt);
}
}
}

namespace Sample\Testo {

use App\Checkout\Order;
use Testo\Assert;
use Testo\Test;

#[Test]
final class OrderTest
{
public function placesOrder(): void
{
$order = Order::place(customerId: 42, items: ['sku-1' => 2, 'sku-2' => 1]);

Assert::string($order->number)
->contains('ORD-')
->notContains(' ');

Assert::int($order->total)
->greaterThan(0)
->lessThanOrEqual(1_000_000);

Assert::array($order->lines)
->hasCount(2)
->hasKeys('sku-1', 'sku-2')
->doesNotHaveKeys('sku-3')
->allOf(Order\Line::class);

Assert::same($order->customerId, 42);
Assert::null($order->paidAt);
}
}
}
65 changes: 65 additions & 0 deletions tests/Sandbox/Sample/02_JsonResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

/**
* Slide 2. HTTP API response body: JSON path chains vs manual `json_decode()` and array digging.
*/

namespace Sample\PhpUnit {

use App\Api\Client;
use PHPUnit\Framework\TestCase;

final class UserEndpointTest extends TestCase
{
public function testReturnsUserProfile(): void
{
$body = (new Client())->get('/api/users/42')->getBody()->getContents();

$this->assertJson($body);
$json = \json_decode($body, true, flags: \JSON_THROW_ON_ERROR);

$this->assertIsArray($json);
$this->assertArrayHasKey('data', $json);
$this->assertArrayHasKey('meta', $json);

$this->assertIsArray($json['data']);
$this->assertArrayHasKey('id', $json['data']);
$this->assertSame(42, $json['data']['id']);
$this->assertArrayHasKey('email', $json['data']);
$this->assertStringContainsString('@', $json['data']['email']);

$this->assertArrayHasKey('roles', $json['data']);
$this->assertIsArray($json['data']['roles']);
$this->assertCount(2, $json['data']['roles']);
$this->assertContains('admin', $json['data']['roles']);
}
}
}

namespace Sample\Testo {

use App\Api\Client;
use Testo\Assert;
use Testo\Test;

#[Test]
final class UserEndpointTest
{
public function returnsUserProfile(): void
{
$body = (new Client())->get('/api/users/42')->getBody()->getContents();

Assert::json($body)
->isObject()
->hasKeys(['data', 'meta'])
->assertPath('$.data.id', fn($id) => Assert::same($id->decode(), 42))
->assertPath('$.data.email', fn($email) => Assert::string($email->decode())
->contains('@'))
->assertPath('$.data.roles', fn($roles) => Assert::array($roles->decode())
->hasCount(2)
->contains('admin'));
}
}
}
76 changes: 76 additions & 0 deletions tests/Sandbox/Sample/03_ExpectException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

/**
* Slide 3. Expecting an exception with a cause: one `Expect` chain vs `expectException*()` plus try/catch
* for the `previous` exception (PHPUnit has no built-in way to check it).
*/

namespace Sample\PhpUnit {

use App\Billing\PaymentFailed;
use App\Billing\PaymentGateway;
use App\Billing\Wallet;
use PHPUnit\Framework\TestCase;

final class WalletTest extends TestCase
{
public function testRejectsChargeWhenGatewayDeclines(): void
{
$wallet = new Wallet(new PaymentGateway(declineAll: true));

$this->expectException(PaymentFailed::class);
$this->expectExceptionMessage('Charge of 1500 declined');

$wallet->charge(1500);
}

public function testKeepsGatewayErrorAsCause(): void
{
$wallet = new Wallet(new PaymentGateway(declineAll: true));

try {
$wallet->charge(1500);
$this->fail('PaymentFailed was not thrown');
} catch (PaymentFailed $e) {
$this->assertSame(PaymentFailed::DECLINED, $e->getCode());

$previous = $e->getPrevious();
$this->assertInstanceOf(PaymentGateway\Declined::class, $previous);
$this->assertSame('insufficient_funds', $previous->getMessage());
}
}
}
}

namespace Sample\Testo {

use App\Billing\PaymentFailed;
use App\Billing\PaymentGateway;
use App\Billing\Wallet;
use Testo\Expect;
use Testo\Test;

/**
* Testo's `Expect` allows a single chain to assert the exception type, message, code, and cause, without try/catch.
*/
#[Test]
final class WalletTest
{
public function rejectsChargeWhenGatewayDeclines(): never
{
$wallet = new Wallet(new PaymentGateway(declineAll: true));

Expect::exception(PaymentFailed::class)
->withMessage('Charge of 1500 declined')
->withCode(PaymentFailed::DECLINED)
->withPrevious(
PaymentGateway\Declined::class,
static fn($cause) => $cause->withMessage('insufficient_funds'),
);

$wallet->charge(1500);
}
}
}
62 changes: 62 additions & 0 deletions tests/Sandbox/Sample/04_Collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/**
* Slide 4. Collection checks: `every()` / `allOf()` chains vs `foreach` loops full of assertions.
*/

namespace Sample\PhpUnit {

use App\Catalog\Product;
use App\Catalog\ProductRepository;
use PHPUnit\Framework\TestCase;

final class ProductRepositoryTest extends TestCase
{
public function testFindsPublishedProductsInCategory(): void
{
$products = (new ProductRepository())->findPublished(category: 'books');

$this->assertNotEmpty($products);
$this->assertCount(3, $products);
$this->assertContainsOnlyInstancesOf(Product::class, $products);

foreach ($products as $product) {
$this->assertTrue($product->isPublished());
$this->assertSame('books', $product->category);
$this->assertGreaterThan(0, $product->price);
}

$ids = \array_map(static fn(Product $p) => $p->id, $products);
\sort($ids);
$this->assertSame([7, 12, 31], $ids);
}
}
}

namespace Sample\Testo {

use App\Catalog\Product;
use App\Catalog\ProductRepository;
use Testo\Assert;
use Testo\Test;

#[Test]
final class ProductRepositoryTest
{
public function findsPublishedProductsInCategory(): void
{
$products = (new ProductRepository())->findPublished(category: 'books');

Assert::iterable($products)
->notEmpty()
->hasCount(3)
->allOf(Product::class)
->every(static fn(Product $p) => $p->isPublished() && $p->category === 'books' && $p->price > 0);

Assert::array(\array_map(static fn(Product $p) => $p->id, $products))
->sameElementsAs([7, 12, 31]);
}
}
}
53 changes: 53 additions & 0 deletions tests/Sandbox/Sample/05_MemoryLeak.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

/**
* Slide 5. Memory leaks: `Expect::notLeaks()` vs `WeakReference` plumbing by hand.
*/

namespace Sample\PhpUnit {

use App\Events\EventDispatcher;
use App\Events\OrderPlacedListener;
use PHPUnit\Framework\TestCase;

final class EventDispatcherTest extends TestCase
{
public function testReleasesListenerAfterRemoval(): void
{
$dispatcher = new EventDispatcher();
$listener = new OrderPlacedListener();
$dispatcher->listen('order.placed', $listener);

$dispatcher->forget('order.placed', $listener);

$ref = \WeakReference::create($listener);
unset($listener);
\gc_collect_cycles();
$this->assertNull($ref->get(), 'Listener is still referenced by the dispatcher');
}
}
}

namespace Sample\Testo {

use App\Events\EventDispatcher;
use App\Events\OrderPlacedListener;
use Testo\Expect;
use Testo\Test;

#[Test]
final class EventDispatcherTest
{
public function releasesListenerAfterRemoval(): void
{
$dispatcher = new EventDispatcher();
$listener = new OrderPlacedListener();
$dispatcher->listen('order.placed', $listener);
Expect::notLeaks($listener);

$dispatcher->forget('order.placed', $listener);
}
}
}
48 changes: 48 additions & 0 deletions tests/Sandbox/Sample/06_DataSetInline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/**
* Slide 6. A handful of fixed cases inline: PHPUnit `#[TestWith]` (case name since 11.5) vs Testo `#[DataSet]`.
* The shapes are on par here; the difference shows up in slides 7 and 8.
*/

namespace Sample\PhpUnit {

use App\Pricing\Discount;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;

final class DiscountTest extends TestCase
{
#[TestWith([4_999, 4_999], 'below threshold')]
#[TestWith([5_000, 4_500], 'exactly threshold')]
#[TestWith([20_000, 17_000], 'silver tier')]
#[TestWith([100_000, 80_000], 'gold tier')]
public function testAppliesTieredDiscount(int $subtotal, int $expected): void
{
$this->assertSame($expected, Discount::apply($subtotal));
}
}
}

namespace Sample\Testo {

use App\Pricing\Discount;
use Testo\Assert;
use Testo\Data\DataSet;
use Testo\Test;

#[Test]
final class DiscountTest
{
#[DataSet([4_999, 4_999], 'below threshold')]
#[DataSet([5_000, 4_500], 'exactly threshold')]
#[DataSet([20_000, 17_000], 'silver tier')]
#[DataSet([100_000, 80_000], 'gold tier')]
public function appliesTieredDiscount(int $subtotal, int $expected): void
{
Assert::same(Discount::apply($subtotal), $expected);
}
}
}
Loading
Loading