Skip to content

Commit

Permalink
Merge branch 'release/3.0.0-rc.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
lindyhopchris committed Jan 18, 2025
2 parents b367644 + ee33cf8 commit 9003a4c
Show file tree
Hide file tree
Showing 22 changed files with 917 additions and 30 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. This projec

## Unreleased

## [3.0.0-rc.2] - 2025-01-18

### Added

- New test classes for driven ports and the domain event dispatcher. These are intended to make setting up unit and
integration tests easier. They can also be used as fakes while you build your real implementation. The classes are in
the `Testing` namespace and are:
- `Testing\FakeDomainEventDispatcher`
- `Testing\FakeExceptionReporter`
- `Testing\FakeOutboundEventPublisher`
- `Testing\FakeQueue`
- `Testing\FakeUnitOfWork`
- Properties on message classes can now be marked as sensitive so that they are not logged. This is an alternative to
having to implement the `ContextProvider` interface. Mark a property as sensitive using the
`CloudCreativity\Modules\Toolkit\Loggable\Sensitive` attribute.

## [3.0.0-rc.1] - 2025-01-12

### Added
Expand Down
23 changes: 20 additions & 3 deletions docs/guide/application/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,26 @@ $bus->through([LogMessageDispatch::class]);
When logging that the command is being dispatched, we log all the public properties of the command message as log
context. This is useful for debugging, as it allows you to see the data that was provided.

However, there can be scenarios where you want to control what context is logged. A good example is a command message
that has sensitive customer data on it that you do not want to end up in your logs. To control the log context,
implement the `ContextProvider` interface on your command message:
However, there may be scenarios where a property should not be logged, e.g. because it contains sensitive information.
In this scenario, use the `Sensitive` attribute on the property, and it will not be logged:

```php
use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use CloudCreativity\Modules\Toolkit\Loggable\Sensitive;

final readonly class CancelAttendeeTicketCommand implements Command
{
public function __construct(
public Identifier $attendeeId,
#[Sensitive] public Identifier $ticketId,
public CancellationReasonEnum $reason,
) {
}
}
```

If you need full control over the log context, implement the `ContextProvider` interface on your command message:

```php
use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier;
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/application/integration-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -876,9 +876,9 @@ $middleware->bind(
The use of this middleware is identical to that described in the [Commands chapter.](./commands#logging)
See those instructions for more information, such as configuring the log levels.

Additionally, if you need to customise the context that is logged for an integration event then implement the
`ContextProvider` interface on your integration event message. See the example in the
[Commands chapter.](./commands#logging)
Additionally, you can customise the context that is logged for an event. To exclude properties, mark them with the
`Sensitive` attribute. Alternatively, if you need full control over the context, implement the `ContextProvider`
interface on your integration event. See the examples in the [Commands chapter.](./commands#logging)

### Writing Middleware

Expand Down
10 changes: 6 additions & 4 deletions docs/guide/application/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ final class QueryBus extends QueryDispatcher implements Port

### Creating a Query Bus

The query dispatcher class that your implementation extends (in the above example) allows you to build a query bus
The query dispatcher class that your implementation extends (in the above example) allows you to build a query bus
specific to your domain. You do this by:

1. Binding query handler factories into the query dispatcher; and
Expand Down Expand Up @@ -193,7 +193,8 @@ final class QueryBusProvider
}
```

Adapters in the presentation and delivery layer will use the driving ports. Typically this means we need to bind the port into a service container. For example, in Laravel:
Adapters in the presentation and delivery layer will use the driving ports. Typically this means we need to bind the
port into a service container. For example, in Laravel:

```php
namespace App\Providers;
Expand Down Expand Up @@ -388,8 +389,9 @@ $middleware->bind(
The use of this middleware is identical to that described in the [Commands chapter.](./commands#logging)
See those instructions for more information, such as configuring the log levels.

Additionally, if you need to customise the context that is logged for a query then implement the
`ContextProvider` interface on your query message. See the example in the [Commands chapter.](./commands#logging)
Additionally, you can customise the context that is logged for a query. To exclude properties, mark them with the
`Sensitive` attribute. Alternatively, if you need full control over the context, implement the `ContextProvider`
interface on your query message. See the examples in the [Commands chapter.](./commands#logging)

### Writing Middleware

Expand Down
28 changes: 27 additions & 1 deletion docs/guide/application/units-of-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,8 @@ final readonly class CancelAttendeeTicketHandler implements

### Integration Event Handlers

As explained in the [integration events chapter](../application/integration-events#strategies), there are several strategies that
As explained in the [integration events chapter](../application/integration-events#strategies), there are several
strategies that
can be used to handle inbound events.

If you dispatch a command as a result of the inbound event, you do not need to worry about the unit of work
Expand Down Expand Up @@ -488,4 +489,29 @@ final readonly class OrderWasFulfilledHandler implements
];
}
}
```

## Testing

We provide a fake unit of work that can be used in your tests. This is the
`CloudCreativity\Modules\Testing\FakeUnitOfWork` class.

This is a fully working implementation that will fake starting and committing transactions. It will also re-attempt the
transaction if it fails and the number of attempts is greater than one.

If you need to check the sequence of what happened in the unit of work, this can be done via the `$sequence` property on
the fake unit of work. For example, if you wanted to check that the unit of work was attempted twice and succeeded on
the second attempt:

```php
use CloudCreativity\Modules\Testing\FakeUnitOfWork;

$unitOfWork = new FakeUnitOfWork();

// execute work that uses the unit of work

$this->assertSame(
['attempt:1', 'rollback:1', 'attempt:2', 'commit:2'],
$unitOfWork->sequence,
);
```
46 changes: 28 additions & 18 deletions docs/guide/domain/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,42 +356,52 @@ any integration events at all.
Your testing of aggregates and entities should encompass asserting that they dispatch the correct domain events, in the
correct scenarios. If you are following our domain services pattern shown earlier in this chapter, this is easy to do.

In your aggregate test case, setup and tear down the services:
We also provide a `FakeDomainEventDispatcher` that you can use in your tests. This is a simple implementation of the
domain event dispatcher that allows you to assert that events are dispatched.

Putting the two together, the following is a good pattern for testing domain events:

```php
use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher as IDomainEventDispatcher;
use CloudCreativity\Modules\Testing\FakeDomainEventDispatcher;

class AttendeeTest extends TestCase
{
private DomainEventDispatcher&MockObject $events;
private FakeDomainEventDispatcher $dispatcher;

protected function setUp(): void
{
parent::setUp();
$this->events = $this->createMock(DomainEventDispatcher::class);
Services::setEvents(fn() => $this->events);

$this->dispatcher = new class () extends FakeDomainEventDispatcher implements IDomainEventDispatcher {};

Services::setEvents(fn () => $this->dispatcher);
}

protected function tearDown(): void
{
Services::tearDown();
unset($this->events);
unset($this->dispatcher);
parent::tearDown();
}
}
```

Then in the relevant test:
Then you can assert that events were dispatched via the fake dispatcher's `$events` property:

```php
$this->assertCount(2, $this->dispatcher->events);
```

If you are only expecting exactly one event to be dispatched, use the `sole()` helper method:

```php
$this->events
->expects($this->once())
->method('dispatch')
->with($this->callback(
function (AttendeeTicketWasCancelled $event) use ($eventId, $attendeeId, $ticketId, $reason): bool {
$this->assertObjectEquals($eventId, $event->eventId);
$this->assertObjectEquals($attendeeId, $event->attendeeId);
$this->assertObjectEquals($ticketId, $event->ticketId);
$this->assertSame($reason, $event->reason);
return true;
},
));
$expected = new AttendeeTicketWasCancelled(
eventId: $eventId,
attendeeId: $attendeeId,
ticketId: $ticketId,
reason: $reason,
);

$this->assertEquals($expected, $this->dispatcher->sole());
```
27 changes: 27 additions & 0 deletions docs/guide/infrastructure/exception-reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,31 @@ final readonly class ExceptionReporterAdapter implements
$this->handler->report($ex);
}
}
```

## Testing

We provide a fake exception reporter that you can use in tests. This is the
`CloudCreativity\Modules\Testing\FakeUnitOfWork` class.

You can access any exceptions that were reported via the `$reported` property:

```php
use CloudCreativity\Modules\Testing\FakeExceptionReporter;

$reporter = new FakeExceptionReporter();

// do work that might throw an exception

$this->assertCount(2, $reporter->reported);
```

If you expect exactly one exception to be reported, use the `sole()` helper:

```php
$expected = new \LogicException('Boom!');

// do work

$this->assertSame($expected, $reporter->sole());
```
25 changes: 25 additions & 0 deletions docs/guide/infrastructure/publishing-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,28 @@ If you're writing middleware that is only meant to be used for a specific integr
`OutboundEventMiddleware` interface. Instead, use the same signature but change the event type-hint to the event class
your middleware is designed to be used with.
:::

## Testing

We provide a fake outbound event publisher that you can use in tests. This is the
`CloudCreativity\Modules\Testing\FakeOutboundEventPublisher` class.

You can access any published events via the `$events` property:

```php
use CloudCreativity\Modules\Testing\FakeOutboundEventPublisher;

$publisher = new FakeOutboundEventPublisher();

// do work that might publish an event

$this->assertCount(2, $publisher->events);
```

If you expect exactly one integration event to be published, use the `sole()` helper:

```php
$expected = new SomeIntegrationEvent();

$this->assertEquals($expected, $publisher->sole());
```
25 changes: 25 additions & 0 deletions docs/guide/infrastructure/queues.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,3 +495,28 @@ If you're writing middleware that is only meant to be used for a specific comman
the `QueueMiddleware` interface. Instead, use the same signature but change the type-hint for to the specific command
message your middleware is designed for.
:::

## Testing

We provide a fake queue that you can use in tests. This is the `CloudCreativity\Modules\Testing\FakeQueue` class.

You can access any queued commands via the `$commands` property:

```php
use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue as Port;
use CloudCreativity\Modules\Testing\FakeQueue;

$queue = new class () extends FakeQueue implements Port {};

// do work that might queue a command

$this->assertCount(2, $queue->commands);
```

If you expect exactly one command to be queued, use the `sole()` helper:

```php
$expected = new SomeCommand();

$this->assertEquals($expected, $queue->sole());
```
50 changes: 50 additions & 0 deletions src/Testing/FakeDomainEventDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/*
* Copyright 2025 Cloud Creativity Limited
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

declare(strict_types=1);

namespace CloudCreativity\Modules\Testing;

use CloudCreativity\Modules\Contracts\Domain\Events\DomainEvent;
use CloudCreativity\Modules\Contracts\Domain\Events\DomainEventDispatcher;
use LogicException;

class FakeDomainEventDispatcher implements DomainEventDispatcher
{
/**
* @var array<DomainEvent>
*/
public array $events = [];

/**
* @inheritDoc
*/
public function dispatch(DomainEvent $event): void
{
$this->events[] = $event;
}

/**
* Expect a single event to be dispatched and return it.
*
* @return DomainEvent
*/
public function sole(): DomainEvent
{
if (count($this->events) === 1) {
return $this->events[0];
}

throw new LogicException(sprintf(
'Expected one event to be dispatched but there are %d events.',
count($this->events),
));
}
}
Loading

0 comments on commit 9003a4c

Please sign in to comment.