Skip to content

Commit

Permalink
Merge pull request #27 from tonysm/better-testing
Browse files Browse the repository at this point in the history
Better Testing
  • Loading branch information
tonysm authored Jul 18, 2021
2 parents 111e605 + 5ab6704 commit f9aafc0
Show file tree
Hide file tree
Showing 10 changed files with 542 additions and 29 deletions.
111 changes: 83 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,52 +710,107 @@ if (Turbo::isTurboNativeVisit()) {
<a name="testing-helpers"></a>
### Testing Helpers

There is a [companion package](https://github.com/tonysm/turbo-laravel-test-helpers) that you may use as a dev dependency on your application to help with testing your apps using Turbo Laravel. First, install the package:
There are two aspects of your application using Turbo Laravel that are specific this approach itself:

```bash
composer require tonysm/turbo-laravel-test-helpers --dev
```
1. **Turbo Stream HTTP responses.** As you return Turbo Stream responses from your route handlers/controllers to be applied by Turbo itself; and
1. **Turbo Stream broadcasts.** Which is the side-effect of certain model changes or whenever you call `$model->broadcastAppend()` on your models, for instance.

We're going to cover both of these scenarios here.

#### Making Turbo & Turbo Native HTTP requests

To enhance your testing capabilities here, Turbo Laravel adds a couple of macros to the TestResponse that Laravel uses under the hood. The goal is that testing Turbo Stream responses is as convenient as testing regular HTTP responses.

To mimic Turbo requests, which means sending a request setting the correct Content-Type in the `Accept:` HTTP header, you need to use the `InteractsWithTurbo` trait to your testcase. Now you can mimic a Turbo HTTP request by using the `$this->turbo()` method before you make the HTTP call itself. You can also mimic Turbo Native specific requests by using the `$this->turboNative()` also before you make the HTTP call. The first method will add the correct Turbo Stream content type to the `Accept:` header, and the second method will add Turbo Native `User-Agent:` value.

These methods are handy when you are conditionally returning Turbo Stream responses based on the `request()->wantsTurboStream()` helper, for instance. Or when using the `@turbonative` or `@unlessturbonative` Blade directives.

And then you will be able to test your application like:
### Testing Turbo Stream HTTP Responses

``` php
use Tonysm\TurboLaravelTestHelpers\Testing\InteractsWithTurbo;
You can test if you got a Turbo Stream response by using the `assertTurboStream`. Similarly, you can assert that your response is _not_ a Turbo Stream response by using the `assertNotTurboStream()` macro:

class ExampleTest extends TestCase
```php
use Tonysm\TurboLaravel\Testing\InteractsWithTurbo;

class CreateTodosTest extends TestCase
{
use InteractsWithTurbo;

/** @test */
public function turbo_stream_test()
public function creating_todo_from_turbo_request_returns_turbo_stream_response()
{
$response = $this->turbo()->post('my-route');
$response = $this->turbo()->post(route('todos.store'), [
'content' => 'Test the app',
]);

$response->assertTurboStream();
}

$response->assertHasTurboStream($target = 'users', $action = 'append');
/** @test */
public function creating_todo_from_regular_request_does_not_return_turbo_stream_response()
{
// Notice we're not chaining the `$this->turbo()` method here.
$response = $this->post(route('todos.store'), [
'content' => 'Test the app',
]);

$response->assertDoesntHaveTurboStream($target = 'empty_users', $action = 'remove');
$response->assertNotTurboStream();
}
}
```

/** @test */
public function turbo_native_shows()
The controller for such response would be something like this:

```php
class TodosController
{
public function store()
{
$response = $this->turboNative()->get('my-route');
$todo = auth()->user()->todos()->create(request()->validate([
'content' => ['required'],
]));

$response->assertSee('Only rendered in Turbo Native');
if (request()->wantsTurboStream()) {
return response()->turboStream($todo);
}

return redirect()->route('todos.index');
}
}
```

Check out the [package repository](https://github.com/tonysm/turbo-laravel-test-helpers) if you want to know more about it.
#### Fluent Turbo Stream Testing

All model's broadcast will dispatch a `Tonysm\TurboLaravel\Jobs\BroadcastAction` job (either to a worker or process them immediately). You may also use that to test your broadcasts like so:
You can get specific on your Turbo Stream responses by passing a callback to the `assertTurboStream(fn)` method. This can be used to test that you have a specific Turbo Stream tag being returned, or that you're returning exactly 2 Turbo Stream tags, for instance:

```php
use App\Models\Post;
use Tonysm\TurboLaravel\Jobs\BroadcastAction;
/** @test */
public function create_todos()
{
$this->get(route('todos.store'))
->assertTurboStream(fn (AssertableTurboStream $turboStreams) => (
$turboStreams->has(2)
&& $turboStreams->hasTurboStream(fn ($turboStream) => (
$turboStream->where('target', 'flash_messages')
->where('action', 'prepend')
->see('Todo was successfully created!')
))
&& $turboStreams->hasTurboStream(fn ($turboStream) => (
$turboStream->where('target', 'todos')
->where('action', 'append')
->see('Test the app')
))
));
}
```

#### Testing Turbo Stream Broadcasts

use function Tonysm\TurboLaravel\turbo_channel;
Every broadcast will be dispatched using the `Tonysm\TurboLaravel\Jobs\BroadcastAction` job (either to a worker or process synchronously). You may also use that to test your broadcasts like so:

```php
use App\Models\Todo;
use Tonysm\TurboLaravel\Jobs\BroadcastAction;

class CreatesCommentsTest extends TestCase
{
Expand All @@ -764,20 +819,20 @@ class CreatesCommentsTest extends TestCase
{
Bus::fake(BroadcastAction::class);

$post = Post::factory()->create();
$todo = Todo::factory()->create();

$this->turbo()->post(route('posts.comments.store', $post), [
'content' => 'Hello, World',
$this->turbo()->post(route('todos.comments.store', $todo), [
'content' => 'Hey, this is really nice!',
])->assertTurboStream();

Bus::assertDispatched(function (BroadcastAction $job) use($post) {
Bus::assertDispatched(function (BroadcastAction $job) use ($todo) {
return count($job->channels) === 1
&& $job->channels[0]->name === sprintf('private-%s', $post->broadcastChannel())
&& $job->channels[0]->name === sprintf('private-%s', $todo->broadcastChannel())
&& $job->target === 'comments'
&& $job->action === 'append'
&& $job->partial === 'comments._comment'
&& $job->partialData['comment']->is(
$post->comments->first()
$todo->comments->first()
);
});
}
Expand All @@ -795,7 +850,7 @@ Try the package out. Use your Browser's DevTools to inspect the responses. You w
Make something awesome!

## Testing
## Testing the Package

```bash
composer test
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/TurboInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public function handle()
{
$this->updateNodePackages(function ($packages) {
return [
'@hotwired/turbo' => '^7.0.0-beta.7',
'@hotwired/turbo' => '^7.0.0-beta.8',
'laravel-echo' => '^1.10.0',
'pusher-js' => '^7.0.2',
] + $packages;
Expand Down
53 changes: 53 additions & 0 deletions src/Testing/AssertableTurboStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Tonysm\TurboLaravel\Testing;

use Closure;
use Illuminate\Support\Collection;
use PHPUnit\Framework\Assert;

class AssertableTurboStream
{
/** @var Collection */
public $turboStreams;

public function __construct(Collection $turboStreams)
{
$this->turboStreams = $turboStreams;
}

public function has(int $expectedTurboStreamsCount): self
{
Assert::assertCount($expectedTurboStreamsCount, $this->turboStreams);

return $this;
}

public function hasTurboStream(Closure $callback = null): self
{
$attrs = collect();

$matches = $this->turboStreams
->mapInto(TurboStreamMatcher::class)
->filter(function ($matcher) use ($callback, $attrs) {
if (! $matcher->matches($callback)) {
$attrs->add($matcher->attrs());

return false;
}

return true;
});

Assert::assertTrue(
$matches->count() === 1,
sprintf(
'Expected to find a matching Turbo Stream for `%s`, but %s',
$attrs->unique()->join(' '),
trans_choice('{0} none was found.|[2,*] :count were found.', $matches->count()),
)
);

return $this;
}
}
18 changes: 18 additions & 0 deletions src/Testing/ConvertTestResponseToTurboStreamCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Tonysm\TurboLaravel\Testing;

use Illuminate\Support\Collection;
use Illuminate\Testing\TestResponse;

class ConvertTestResponseToTurboStreamCollection
{
public function __invoke(TestResponse $response): Collection
{
$parsed = simplexml_load_string(<<<XML
<xml>{$response->content()}</xml>
XML);

return collect(json_decode(json_encode($parsed), true)['turbo-stream']);
}
}
21 changes: 21 additions & 0 deletions src/Testing/InteractsWithTurbo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Tonysm\TurboLaravel\Testing;

use Tonysm\TurboLaravel\Turbo;

/**
* @mixin \Illuminate\Foundation\Testing\Concerns\MakesHttpRequests
*/
trait InteractsWithTurbo
{
public function turbo(): self
{
return $this->withHeader('Accept', Turbo::TURBO_STREAM_FORMAT);
}

public function turboNative(): self
{
return $this->withHeader('User-Agent', 'Turbo Native Android; Mozilla: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.3 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/43.4');
}
}
Loading

0 comments on commit f9aafc0

Please sign in to comment.