Automated regression testing checks that agreed behavior still holds after software changes. A useful test records the starting conditions, performs an action and compares the result with an independently chosen expectation. A green result is evidence about those assertions and conditions, not a guarantee that the application has no defects.
The ISTQB Foundation Level syllabus distinguishes confirming a repaired defect from checking for unintended consequences elsewhere. Repeating those regression checks is a useful role for automation.
This guide uses a small Laravel orders-list fixture. The risk is concrete: a query change could include another workspace's orders or ignore the requested status filter. Download the complete regression example and instructions (Markdown) for the isolated test class. The excerpts below belong to that fixture, not to a production endpoint on this website.
Agree the behavior before preserving it
The intended contract is a read-only orders list. An authenticated identity has a server-side workspace assignment. The endpoint returns that workspace's orders as id and status, ordered by ascending ID. An optional filter accepts only open or closed. A request parameter cannot replace the identity's workspace.
Write that rule before writing the expected JSON. Copying whatever the current application returns could preserve an existing defect. If the business wants a different sort order or an additional status, agree the new contract and update the expectation as part of the reviewed change.
The fixture's route is GET /__regression-example/orders. Its identities are supplied through Laravel's test authentication helper. They do not come from a real login, token exchange or membership lookup. This makes the query boundary small enough to examine without claiming that the whole authorization system has been checked.
Choose cases that expose a plausible failure
A list containing only the current workspace's data cannot reveal a missing workspace restriction. Seed a foreign row deliberately. Specify the expected sequence and order the query explicitly rather than depending on the database's incidental return order.
The fixture uses four orders: IDs 10, 30 and 40 belong to workspace 7; ID 20 belongs to workspace 9. ID 30 is closed and the others are open. Two fixture identities belong to different workspaces.
| Case | Expected observation | Failure it can expose |
|---|---|---|
| Workspace 7 identity supplies workspace_id=9 | Only IDs 10, 30 and 40, in that order. | The query trusts a requested workspace or loses its workspace restriction. |
| Workspace 7 requests open orders | Only IDs 10 and 40. | The filter is ignored or foreign open orders are included. |
| Workspace 9 identity requests its list | Only ID 20. | The workspace is hard-coded to the first test identity. |
| Unauthenticated request | HTTP 401. | The route accepts a request without an authenticated identity. |
| Authenticated request with unsupported status | HTTP 422 with a status validation error. | The request bypasses the stated filter validation. |
These cases follow the risk, not a target test count. A dedicated closed-filter case and an empty-results case remain useful additions. Other applications may need pagination, archived records, role restrictions or revoked membership; this fixture does not implement them.
Keep the fixture deterministic and separate
The complete class uses a dedicated SQLite :memory: connection and creates its own identity and order tables. It seeds fixed IDs and statuses for each test, without running application migrations or using production records. A dedicated guard supplies fixture-only identities through actingAs.
Laravel's HTTP testing documentation explains that these requests are simulated internally. The test client runs the application's request handling and inspects the response without opening a live network connection. Each case here makes one request.
That setup makes the examples repeatable while exercising a real database query. It also makes a boundary explicit: the test uses SQLite, not the production database engine. It cannot establish another engine's query plans, locking behavior or migration compatibility.
The route excerpt below belongs inside the full class's setup. Its imports, guard, connection and table preparation are included in the download; it is not a standalone route to paste into a live application.
Route::get(self::PATH, function (Request $request) {
$validated = $request->validate([
'status' => ['sometimes', 'required', 'string', Rule::in(['open', 'closed'])],
]);
$query = DB::connection(self::CONNECTION)
->table('regression_example_orders')
->where('workspace_id', $request->user()->workspace_id);
if (isset($validated['status'])) {
$query->where('status', $validated['status']);
}
return response()->json([
'data' => $query->orderBy('id')->get(['id', 'status']),
]);
})->middleware('auth:'.self::GUARD);
Trace the order of decisions: establish the authenticated identity, validate the optional filter, constrain the database query to that identity's workspace and return the chosen fields in a defined order. Client input has a narrow role: choosing a permitted status.
Assert the response that matters
A successful status alone would allow the route to return the wrong orders. The representative test below checks the exact response, including which rows appear, their status values and the array order.
public function test_lists_only_its_workspace_in_id_order(): void
{
$this->actingAs($this->fixtureUser(101), self::GUARD)
->getJson(self::PATH.'?workspace_id=9')
->assertOk()
->assertExactJson(['data' => [
['id' => 10, 'status' => 'open'],
['id' => 30, 'status' => 'closed'],
['id' => 40, 'status' => 'open'],
]]);
}
The expected rows are written explicitly rather than derived from the same query under test. Reusing the production selection logic to calculate the expectation could reproduce its mistake on both sides of the comparison.
For this small read-only response, exact JSON is a useful boundary. A larger response may contain irrelevant timestamps or generated identifiers; choose stable assertions that preserve the required behavior. For a workflow that writes records, also inspect the committed state and relevant side effects. An HTTP success message alone does not establish that the intended change was saved.
Show that the check detects a meaningful change
We executed the complete fixture with PHP 8.3.6, Laravel 12.40.2, PHPUnit 11.5.44 and SQLite 3.45.1. All five tests passed, with 10 assertions. The exact route and test excerpts above come from that exercised class.
In an isolated copy, we removed only the query's workspace restriction and reran the unchanged tests. Three tests failed because the returned JSON included orders from another workspace. The guest and invalid-status cases still passed. That result connects the failed assertions to the missing scope rather than to a broken test environment.
The deliberate change is useful because it corresponds to the risk the test was written to expose. A test that fails only because a file cannot load does not demonstrate that it detects an incorrect orders list. Inspect the failure's expected and actual values, alongside the process exit status.
This is a narrow mutation check: alter one behavior, leave the tests unchanged and confirm that they reject the altered result. It does not measure the quality of every assertion or establish comprehensive tenant isolation. The full fixture and its setup remain available for review rather than asking readers to rely on a pass count alone.
Use failures to make a decision
When a regression fails after a change, first determine whether the environment, fixture or application behavior changed. Reproduce the failing case using the recorded revision and dependencies. Compare the observed result with the agreed contract before editing either side.
If behavior changed accidentally, correct the implementation and rerun the check. If the change is intentional, review the requirement and expectation together. Updating a snapshot or expected response merely to make the suite green can erase the protection the test was meant to provide.
Run the focused cases during development, then the applicable broader suite before release. The Laravel CI/CD guide covers connecting checks to a reviewed revision and release process. Scheduling a test in CI does not expand what that test actually exercises.
Keep the remaining evidence visible
This fixture checks an application HTTP response and SQLite query through Laravel's test client. It does not run a browser, exercise deployment networking, perform a security audit or measure load. It also does not assess accessibility or whether people understand the orders interface.
Human observation addresses different questions, as the user testing and usability testing guide explains. A correct filtered JSON response cannot show whether a user notices that a filter is active or can complete their task.
Use the QA testing brief (Markdown) to record the workflow, exclusions, evidence and owner of the next check. The software QA testing services guide helps turn those needs into a bounded scope with reviewable deliverables.
