
-
- Testing Page Response Status
- Testing Page Response Text
- Testing Page Response View
- Testing Page Response JSON
- Testing Against The Database
- Testing Validation
- Testing Models / Relationships
- Testing Mails
- Testing Mail Content
- Testing Jobs & Queues
- Testing Notifications
- Testing Actions
- Testing Middlewares
- Testing File Uploads
- Testing Exceptions
- Testing Units (Unit Tests)
- Faking HTTP Calls
- Testing HTTP Calls
- Mocking Dependencies
- Testing Commands
Testing Page Response Status
It makes sure a page responds with the correct HTTP status code, primarily a 200 response.
Testing page response is one of the simplest tests to write; still, it is extremely useful.
it('gives back a successful response for home page', function () {
$this->get('/')->assertOk();
});
This test is similar to the first page response test. We also test the response, but this time we are interested in the content of the response.
Testing Page Response Text
it('lists products', function () {
// Arrange
$firstProduct = Product::factory()->create();
$secondProduct = Product::factory()->create();
// Act & Assert
$this->get('/')
->assertOk()
->assertSeeTextInOrder([
$firstProduct->title,
$secondProduct->title,
]);
});
Here you also can be more specific, like when you only want to show released products.
Here we are ensuring we see our product titles on the home page. This is useful if you load the products from the database and ensure they are shown.
it('lists released products', function () {
// Arrange
$releasedProduct = Product::factory()
->released()
->create();
$draftProduct = Product::factory()
->create();
// Act & Assert
$this->get('/')
->assertOk()
->assertSeeText($releasedProduct->title)
->assertDontSeeText($draftProduct->title);
});
Testing Page Response View
it('returns correct view', function() {
// Act & Assert
$this->get('/')
->assertOk()
->assertViewIs('home');
});
it('returns correct view', function() {
// Act & Assert
$this->get('/')
->assertOk()
->assertViewIs('home')
->assertViewHas('products');
});
Testing Page Response JSON
Often you want to return JSON data from your API. This is where you can use Laravel’s JSON helpers, like the assertJson method.
it('returns all products as JSON', function () {
// Arrange
$product = Product::factory()->create();
$anotherProduct = Product::factory()->create();
// Act & Assert
$this->post('api/products')
->assertOk()
->assertJson([
[
'title' => $product->title,
'description' => $product->description,
],
[
'title' => $anotherProduct->title,
'description' => $anotherProduct->description,
],
]);
});
Testing Against The Database
it('stores a product', function () {
// Act
$this->actingAs(User::factory()->create())
->post('product', [
'title' => 'Product name',
'description' => 'Product description',
])->assertSuccessful();
// Assert
$this->assertDatabaseCount(Product::class, 1);
$this->assertDatabaseHas(Product::class, [
'title' => 'Product name',
'description' => 'Product description',
]);
});
Testing Validation
Validation is a crucial part of many applications. You want to make sure that only valid data can be submitted. By default, Laravel sends validation errors back to the user, which we can check with the assertInvalid method.
it('requires the title', function () {
// Act
$this->actingAs(User::factory()->create())
->post('product', [
'description' => 'Product description',
])->assertInvalid(['title' => 'required']);
});
it('requires the description', function () {
// Act
$this->actingAs(User::factory()->create())
->post('product', [
'title' => 'Product name',
])->assertInvalid(['description' => 'required']);
});
When dealing with many validation rules, using datasets can be pretty helpful. This can clean up your tests a lot.
it('requires title and description tested with a dataset', function($data, $error) {
// Act
$this->actingAs(User::factory()->create())
->post('product', $data)->assertInvalid($error);
})->with([
'title required' => [['description' => 'text'], ['title' => 'required']],
'description required' => [['title' => 'Title'], ['description' => 'required']],
]);
Testing Models / Relationships
it('has products', function () {
// Arrange
$user = User::factory()
->has(Product::factory())
->create();
// Act
$products = $user->products;
// Assert
expect($products)
->toBeInstanceOf(Collection::class)
->first()->toBeInstanceOf(Product::class);
});
it('only returns released courses for query scope', function () {
// Arrange
Course::factory()->released()->create();
Course::factory()->create();
// Act & Assert
expect(Course::released()->get())
->toHaveCount(1)
->first()->id->toEqual(1);
});
protected function firstName(): Attribute
{
return Attribute::make(
get: fn (string $value) => ucfirst($value),
);
}
it('capitalizes the first character of the first name', function () {
// Arrange
$user = User::factory()->create(['first_name' => 'christoph'])
// Act & Assert
expect($user->first_name)
->toBe('Christoph');
});
Testing Sending Mails
class PublishPodcastController extends Controller
{
public function __invoke(Podcast $podcast)
{
// publish podcast
// ...
Mail::to($podcast->author)->send(new PodcastPublishedMail());
}
}
At the end of this controller, we are sending an email. In our test, we can hit the controller through an endpoint and make sure this email would have been sent.
t('sends email to podcast author', function() {
// Arrange
Mail::fake();
$podcast = Podcast::factory()->create();
// Act
$this->post(route('publish-podcast', $podcast));
// Assert
Mail::assertSent(PodcastPublishedMail::class);
});
Always run the Mail::fake() method at the beginning of your tests when testing emails. This makes sure no actual email is being sent to a user.
Most helper methods like assertSent also accept a callback as the second argument. In our case, it receives the mailable object. It contains all the email data, like the email to which it needs to be sent.
This allows you to make even more assertions, like about the `to-address´ of the email.
Mail::assertSent(PodcastPublishedMail::class, function(PodcastPublishedMail $mail) use ($podcast) {
return $mail->hasTo($podcast->author->email);
});
Testing Mail Content
It also makes sense to test the content of an email. This is especially useful when you have a lot of emails in your application. You want to make sure that the content is correct.
it('contains the product title', function () {
// Arrange
$product = Product::factory()->make();
// Act
$mail = new PaymentSuccessfulMail($product);
// Assert
expect($mail)
->assertHasSubject('Your payment was successful')
->assertSeeInHtml($product->title);
});
Testing Jobs & Queues
it('dispatches an import products job', function () {
// Arrange
Queue::fake();
// Act
$this->post('import');
// Assert
Queue::assertPushed(ImportProductsJob::class);
});
This ensures that my job will be pushed to the queue for a specific trigger, like hitting an endpoint. Again, the Queue::fake() takes care of not pushing a job. We do not want to run the job at this point.
But we still have to test the job, right? Of course. It contains the crucial logic of this feature:
it('imports products', function() {
// Act
(new ImportProductsJob)->handle();
// Assert
$this->assertDatabaseCount(Product::class, 50);
// Make more assertions about the imported data
})
This new test concentrates on the job and what it should do. We trigger the job directly by calling the handle on it, which every job has.
Testing Notifications
it('sends notification about new product', function () {
// Arrange
Notification::fake();
$user = User::factory()->create();
$product = Product::factory()->create();
// Act
$this->artisan(InformAboutNewProductNotification::class, [
'productId' => $product->id,
'userId' => $user->id,
]);
// Assert
Notification::assertSentTo(
[$user], NewProductNotification::class
);
});
In the example above, we test a notification sent to a user when a new product is created. We are using the artisan method to trigger the notification. This is a great way to test notifications triggered by a command.
Again, there is a fake method for the notification facade that makes sure no actual notification is being sent.
Testing Actions
Actions are just simple classes that have one specific job. They are a great way to organize your code, and separate your logic from your controllers to keep them clean. But how do you test them?
Let’s start again from the outside. First, we want to test that our action is called when hitting a specific endpoint.
it('calls add-product-to-user action', function () {
// Assert
$this->mock(AddProductToUserAction::class)
->shouldReceive('handle')
->atLeast()->once();
// Arrange
$product = Product::factory()->create();
$user = User::factory()->create();
// Act
$this->post("purchase/$user->id/$product->i");
});
We can do this by mocking our action class and expecting that the handle method is called. But, again, we are here not interested in what our action does; we want to make sure it is called when we hit our purchase controller.
To make this work, we must ensure that the container resolves our action.
class PurchaseController extends Controller
{
public function __invoke(User $user, Product $product): void
{
app(AddProductToUserAction::class->handle($user, $product);
// Send purchase success email, etc.
}
}
Then we can also test the action itself. Like a job, we call the handle method to trigger the action.
it('adds product to user', function () {
// Arrange
$product = Product::factory()->create();
$user = User::factory()->create();
// Act
(new AddProductToUserAction())->handle($user, $product);
// Assert
expect($user->products)
->toHaveCount(1)
->first()->id->toEqual($product->id);
});
Testing Middlewares
A Middleware is always connected to a request, so I like to test the whole request instead of the isolated middleware.
In our example, we have an archive page and first test the page’s content.
it('shows archived products', function () {
// Arrange
$product = Product::factory()->create();
$archivedProduct = Product::factory()->archived()->create();
// Act & Assert
$this->get(action(PageArchiveController::class))
->assertSeeText($archivedProduct->title)
->assertDontSeeText($product->title);
});
Note:Â Instead of providing the string to the route “get” method manually, I’m using the “action” helper, which gets the route of a controller.
But we also attached a middleware to this route to check if the archive feature is enabled. If not, we will return a 404.
it('returns 404 when archive feature disabled', function() {
// Arrange
Feature::define('archive', false);
// Act & Assert
$this->get(action(PageArchiveController::class))
->assertNotFound();
});
As you can see, we do not test the middleware in isolation but the whole request and desired outcome.
Testing File Uploads
Similar to testing mails, notifications, or jobs, we can also test file uploads with the help of a facade.
it('uploads CSV file', function () {
// Arrange
Storage::fake('uploads');
$file = UploadedFile::fake()->image('statistics.csv');
// Act
$this->post(action(CsvUploadController::class), [
'file' => $file,
])->assertOk();
// Assert
Storage::disk('uploads')->assertExists($file->hashName());
});
Testing Exceptions
Sometimes it is a good thing when an exception is thrown because we intentionally want to stop the execution of our code. We can test that too.
it('stops if at least one account not found', function () {
// Act
$this->artisan(MergeAccountsCommand::class, [
'userId' => 1,
'userToBeMergedId' => 2,
]);
})->throws(ModelNotFoundException::class);
Testing Units (Unit Tests)
Unit tests are great for testing small pieces of code, like a single method. No other dependencies are involved. This makes them very fast and easy to write.
Our example is about a data object. It contains a method that creates a new instance from an a webhook payload.
class UserData
{
public function __construct(
public string $email,
public string $name,
public string $country,
)
{}
public static function fromWebhookPayload(array $webhookCallData): UserData
{
return new self(
$webhookCallData['client_email'],
$webhookCallData['client_name'],
$webhookCallData['client_country'],
);
}
}
In the corresponding test, we only test what this method returns.
it('creates UserData object from paddle webhook call', function () {
// Arrange
$payload = [
'client_email' => 'test@test.at',
'client_name' => 'Christoph Rumpel',
'client_country' => 'AT',
];
// Act
$userData = UserData::fromWebhookPayload($payload);
// Assert
expect($userData)
->email->toBe('test@test.at')
->name->toBe('Christoph Rumpel')
->country->toBe('AT');
});
Faking HTTP Calls
Sometimes you need to make HTTP calls in your application. This could be to fetch data from an external API or to send data to another service. You often want to fake these calls in your tests so you do not have to rely on an external service.
it('import product', function () {
// Arrange
Http::fake();
// Act & Assert
// ...
});
The fake method on the HTTP facade will ensure no real call is made and that the response is always a 200 status code.
But you can be more specific too. For example, we are testing an action that fetches data from an external API and saves it to the database.
it('imports product', function() {
// Arrange
Http::fake([
'https://christoph-rumpel.com/import' => Http::response([
'title' => 'My new product',
'description' => 'This is a description',
]),
]);
$user = User::factory()->create();
// Act
(new ImportProductAction)->handle($user);
// Assert
$this->assertDatabaseHas(Product::class, [
'title' => 'My new product',
'description' => 'This is a description',
]);
});
Testing HTTP Calls
Next to faking HTTP calls, you can also test if a specific call was made. This is useful when you want to ensure that your code makes the right calls.
it('make the right call', function () {
// Arrange
Http::fake();
$user = User::factory()->create();
// Act
(new ImportProductAction)->handle($user);
// Assert
Http::assertSent(function ($request) {
return $request->url() === 'https://christoph-rumpel.com/import'
&& $request['accessToken'] === '123456';
});
});
Mocking Dependencies
When working with code with dependencies, it can be helpful to mock them. This will let you concentrate on the logic of your code and not on the dependencies. This also means mocking can be useful for any kind of tests.
We already did that when testing our action classes, but this works with any dependency. In the following example, we have a controller with two dependencies: a payment provider and a mailer.
Â
class PaymentController extends Controller
{
public function __invoke(PaymentProvider $paymentProvider, Mailer $mailer)
{
$paymentProvider->handle();
$mailer->to(auth()->user())->send(new PaymentSuccessfulMail);
}
}
it('sends payment successful mail', function () {
// Arrange
Mail::fake();
// Expect
$this->mock(PaymentProvider::class)
->shouldReceive('handle')
->once();
// Act
$this->post('payment');
// Assert
Mail::assertSent(PaymentSuccessfulMail::class);
});
Testing Commands
it('merges two accounts', function () {
// Arrange
$user = User::factory()->create();
$userToBeMerged = User::factory()->create();
// Act
$this->artisan(MergeAccountsCommand::class, [
'userId' => $user->id,
'userToBeMergedId' => $userToBeMerged->id,
]);
// Assert
$this->assertDatabaseCount(User::class, 1);
$this->assertDatabaseHas(User::class, [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]);
});
Most interesting is the artisan method, which we can call in any Laravel test. It will trigger the command and run it. We can pass the arguments and options to the command as an array. The rest is very similar to other tests.
But there are some more things we can do with commands. We can also expect output of a command like questions or information messages.
it('asks for user ids', function() {
// Arrange
$user = User::factory()->create();
$userToBeMerged = User::factory()->create();
// Act & Assert
$this->artisan(MergeAccountsCommand::class)
->expectsQuestion('Please provide the user ID of the user you want to keep', $user->id)
->expectsQuestion('Please provide the user ID of the user you want to merge', $userToBeMerged->id)
->expectsOutput('Accounts merged successfully')
->assertSuccessful();
});
As you can see in the example above, this is very useful for testing commands that ask for user input. We can also expect output and make sure the command was successful, which means an exit code of 0.