How I Test Livewire Components
#laravel #testingWhile working with Livewire, I came across a few situations where testing components didn't feel right. I explain today how I solved those issues and what my workflow looks like now.
Intro
To show you how I test my Livewire components now, I need to take you on a little journey. It will give you a better understanding of what I was missing and how I came up with my solutions.
In the last months, I have been working more and more with Laravel Livewire. I love this way of developing and handling so much stuff on the PHP side of things. One of the projects is called Refactoring PHP,
and it will be a platform where you can share code snippets. Other users can then add suggestions for refactored versions of this code. You can check out the project by looking at my past live streams.
Long story short, I was using Livewire a lot lately and ran into a few scenarios bugged me while testing.
For this article, we will make use of a little Feedback form example. Here is the very basic Livewire component.
class FeedbackForm extends Component
{
public string $message = '';
public function render()
{
return view('livewire.feedback-form');
}
public function send(): void
{
Mail::to('taylor@laravel.com')->send(new FeedbackMail($this->message));
}
}
If A Component Is Tested But Not Shown, Does it Exist?
When you create Livewire components by test-driven development, you start writing tests for the component you wish you had. Livewire provides some nice testing helpers that we can make use of. A test for our component might look something like this:
public function it_sends_feedback(): void
{
Mail::fake();
Livewire::test(FeedbackForm::class)
->set('message', 'Test Message')
->call('submit');
Mail::assertSent(FeedbackMail::class);
}
In this test, we are making sure we can set the message
property and call the submit
method of our component class. Then we also check if the email was sent. Tests are working, and everything looks good. But then we open the browser, and the page is empty
. We haven't added our component to a view or route yet. Of course, we could add it, and everything would work. Still, I wanted to test this behavior first. All of our efforts to test the component are worthless if the component is not used. So with the next test, we want to make sure it gets included in another view.
Our feedback form should be added to a Laravel feedback
view file. Luckily, Livewire also adds a test assertion for that. But this one can now be used inside a Laravel feature test like this one:
public function it_contains_feedback_livewire_component(): void
{
// Assert
$this->get('/')
->assertSeeLivewire('feedback-form');
}
The assertion assertSeeLivewire
makes sure the component is provided in the response of a request—this way, we make sure that it is used.
The Component Is Used, But The Page Is Still Empty
That's precisely what happened. All tests were passing, and we even made sure the component is used; still, the page was empty. What was the issue?
It was a pretty simple one: the component's view file was empty
. Wait, what? We tested our component, and it was working with an empty view file? Correct. Livewire assertion helpers are great at setting properties in the class or calling methods, but those helpers don't interact with the actual view file.
So how can we make sure the component works for real? We need to make sure the feedback form is provided in some way.
The Dirty Solution
Let's assert that we see
the needed fields.
public function it_shows_form_fields(): void
{
Livewire::test(FeedbackForm::class)
->assertSee('Messsage Label')
->assertSee('Submit Button');
}
With assertSee
we look for specific strings in our component view. We could even use assertSeeHtml
and look for the input or form tag. Those solutions work, but I don't like them.
First, they are checking strings that can easily change, like the name of a label or button. Second, this is not quite what we want to test. What is our goal again? We don't care about what strings or tags are being used. We care about the properties and methods of our component. We want to make sure they are connected.
New Assertions To Rescue
Take a look at this new test:
/** @test **/
public function it_has_wired_fields_and_methods(): void
{
// Arrange & Act & Assert
Livewire::test(FeedbackForm::class)
->assertPropertyWired('message')
->assertMethodWired('submit');
}
First, the test name already clarifies what we want to test; the connection between the class and view of our component. The assertPropertyWired
helper makes sure that the property with the name message
is wired inside our view. And by wired, we mean an HTML tag with wire:model="message"
. Similar to this, the assertMethodWired
checks if an HTML tag is connected to a component method through wire:click="send"
.
Wait, what's happening here? What are those assertions I never heard of?
Hello Missing Liverwire Assertions
Since I needed those new helpers myself, I created a tiny package that adds them to Livewire. It's called Missing Livewire Assertions. Here are the additional methods it currently provides:
Livewire::test(FeedbackForm::class)
->assertPropertyWired('message') // Checks if property is wired
->assertMethodWired('submit') // Checks if method is wired
->assertContainsLivewireComponent(CategoriesList::class) // Checks if component contains another Livewire component
->assertContainsBladeComponent(Button::class); // Checks if component contains a blade component
With those additions, I'm now able to test all aspects of a livewire component:
- Test component's logic
- Test component's connection to the view
- Test component is being used somewhere
These rules also help while thinking about where to test what. In my test for the feedback page, I could also test some component's specific behavior. But I want to make a clear distinction here. All tests regarding a component should be placed inside a test only dedicated to that component. The only exception is checking if it is used; this will be part of our page test again.
Today I released the first version of Missing Livewire Assertions which you can already check out. There are still a few edge-cases I need to take care of before a 1.0 release.
Please give it a look and let me know what you think about it on Twitter.
PS: Thanks a lot to my livestream crew for helping shape the ideas for this package.(Adrian, Gertjan, Felix, Benjaming, etc.)