DEV Community

Cover image for Crush Bugs with Laravel Contract-Based Testing
Isra Skyler
Isra Skyler

Posted on

Crush Bugs with Laravel Contract-Based Testing

As an experienced Laravel developer at Hybrid Web Agency, reliability is core to every project I take on. While testing is mandatory, traditional methods sometimes fall short on complex work with changing requirements.
That's why I've adopted contract-based testing to validate new features and squash bugs before delivery. By defining intended behaviors and expected outputs, contracts ensure my code works as planned - now and later.
This focus on quality has paid dividends for clients. Budgets hold steady as rework decreases. Users enjoy seamless experiences and confidence, with freedom to adjust scope down the line.
In this post, I'll demonstrate how contract testing improved one application. From setup to maintenance, see how shifting left on quality boosts deliverables and strengthens client relationships. Once you know the process - You will be much more confident before you Hire Laravel Developers in Everett.

What are Contract Tests?

Contract testing is an approach that validates public APIs by specifying expected app behaviors rather than implementation details. Unlike unit or integration tests, contracts asserts code works as intended now and handles future unknown changes.

Contracts define just the public methods and properties of a class - its "contract" with external users. Tests then exercise this public API through realistic usage scenarios and assertions. This separates the "contract" from how it's coded internally.

Some key benefits include:

  • Focused on critical behaviors: Tests validate crucial expected inputs/outputs rather than testing all code paths. This focuses on important use cases.

  • Stable over time: Contracts act as documentation and guards against regressions from code changes. Tests still pass as code evolves if output contracts are met.

  • Reader-focused documentation: Contracts double as living documentation developers can reference to understand how to use classes properly without digging through code.

Here is a simple example contract class for validating a common "CountAll" method:

interface PostRepositoryContract {

  public function countAll(): int;

}

class PostRepositoryContractTest extends TestCase {

  public function testCountAllReturnsInteger() {

    $mock = Mockery::mock(PostRepositoryContract::class);

    $count = $mock->countAll();

    $this->assertInternalType('integer', $count);

  }

}
Enter fullscreen mode Exit fullscreen mode

This contract asserts the method returns an integer without concerning how counting is implemented - keeping tests stable over time.

Setting Up Contract Tests in Laravel

Setting up contract tests in Laravel involves just a few simple steps to get your environment configured.

Installing Necessary Packages

The main package needed is PHPUnit. Run composer require --dev phpunit/phpunit to add it as a development dependency.

You'll also want helper packages like Mockery for generating test doubles. composer require --dev mockery/mockery

Configuring the Testing Environment

Add phpunit.xml to your project root with the basic configuration. This tells PHPUnit where to find your tests.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
  <testsuites>
    <testsuite name="App Tests">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>
Enter fullscreen mode Exit fullscreen mode

Generating a Basic Test Stub

Create a tests directory and a sample test file like Feature/UserTest.php. Import necessary classes and traits:

<?php

use PHPUnit\Framework\TestCase;

class FeatureTest extends TestCase
{
  public function setUp(): void
  {
    //...
  }

  public function testExample()
  {

  }

}
Enter fullscreen mode Exit fullscreen mode

Now your environment is ready to start writing focused contract tests!

A Real-World Contract Test Example

To demonstrate contract testing in action, let's look at a real-world example.

Choosing a Class to Test

For this example, we'll focus on a core Repository class that interacts with the database, such as a PostRepository. It retrieves, creates, updates posts and is crucial to our app's functionality.

Defining the Public API Contract

First, we define the public methods and properties the repository exposes with an interface:

interface PostRepositoryInterface {

  public function all();

  public function create(array $data);

  // etc

}
Enter fullscreen mode Exit fullscreen mode

Writing Test Assertions

Next, we create tests that exercise this interface through common usage scenarios:

public function testAllReturnsCollection()
{
  $mock = Mockery::mock(PostRepositoryInterface::class);

  $posts = $mock->all();

  $this->assertInstanceOf(Collection::class, $posts);
}

public function testCreateStoresPost()
{
  $mock = Mockery::mock(PostRepositoryInterface::class);

  $mock->shouldReceive('create')->once();

  $mock->create(['title' => 'Test']);
}
Enter fullscreen mode Exit fullscreen mode

Running Tests

Now simply run phpunit! These contract tests validate our code's public behaviors independently of implementation details.

Let's see how this approach improves our code quality over time.

Common Contracts to Test

There are some key areas of a typical Laravel application that benefit most from contract testing:

Database Models & Repositories

Classes that interact with the database like Eloquent models and repository interfaces are perfect for contract testing. Validate expected behaviors for fetching, updating and relating data without depending on the backend.

API Controllers

API surfaces define your application's public contract with external consumers. Test controller methods adhere to formats, require expected parameters, and return anticipated payloads and status codes.

Mailables & Notifications

Notifications and mailables send critical communications. Contract test these by asserting views and data are rendered properly without transport concerns muddying tests.

Core Application Services

Services encapsulate much of your application's business logic. Test service contracts return expected payloads and exceptions for a variety of input scenarios. This validates coreworkflows independently of UI logic.

By focusing tests on these common artifacts, you can have confidence in critical contracts even as code evolves. Tests act as both validation and living documentation of intentions and boundaries for key classes.

Maintenance and Continuous Testing

Contract tests provide ongoing value beyond initial development:

Refactoring Without Breaking Contracts

Since tests focus on public APIs, internal refactors and optimizations won't cause failures if they don't change public behavior specifications. This allows safe changes over time.

Versioning/Breaking Changes Clearly

Before introducing breaking changes, update contracts by removing/modifying methods or arguments. This signals tests and consumers explicitly to change accordingly.

Integrating with Continuous Integration

Add PHPUnit runs to your CI/CD pipeline. Now contract tests prevent regressions from being deployed automatically. Team members receive immediate feedback from failures.

As codebases evolve rapidly, contracts catch where internal changes alter intended external behaviors. They document restrictions that maintain backwards compatibility over versions.

With contracts, releasing with confidence is attainable. Refactors, features and optimizations land safely while preserving established public interfaces. Tests act as living documentation reflecting how code works and is meant to be used.

Conclusion

Through every phase of development, from initial specs to ongoing maintenance, quality must remain the highest priority. Contract tests provide the structure and assurance that promises to users are kept, even as circumstances change.

By clarifying expectations instead of implementation, contracts make code understandable from any perspective. They facilitate safe evolution, ensuring the present works as before while allowing imagination for what's next.

Most of all, contract testing transforms relationships. Where fear of breakage once restricted progress, shared expectations built on honesty and respect empower risk-taking. Developers and customers walk together toward a same destination, bold yet careful in each step.

This partnership of clarity and care is what raises a simple application to the level of trusted companion. When reliability is guaranteed, so too is the freedom to learn, grow and achieve ever more together. That is the true promise of contract testing in Laravel and beyond.

Top comments (0)