| name | qa-testing-strategy |
| description | Use when writing tests, designing test plans, or verifying bug fixes for the platform. Covers PHPUnit, Pest, Cypress/Playwright, test data factories, multi-tenant isolation testing, and quality gate criteria. |
| user-invocable | false |
| allowed-tools | ["Read","Write","Bash","Grep","Glob"] |
QA Testing Strategy — Platform Testing
Test Pyramid
╱╲
╱ E2E ╲ — 10% (Cypress/Playwright: critical user flows)
╱────────╲
╱Integration╲ — 30% (Feature tests: API endpoints, DB queries)
╱──────────────╲
╱ Unit Tests ╲ — 60% (PHPUnit/Pest: services, models, helpers)
╱────────────────────╲
Unit Test Pattern (Pest/PHPUnit)
use App\Models\Invoice;
use App\Services\InvoiceService;
describe('InvoiceService', function () {
beforeEach(function () {
$this->service = app(InvoiceService::class);
$this->branch = Branch::factory()->create();
$this->actingAs(User::factory()->for($this->branch)->create());
});
it('creates an invoice with line items', function () {
$customer = Customer::factory()->create(['branch_id' => $this->branch->id]);
$product = Product::factory()->create(['branch_id' => $this->branch->id, 'price' => 100]);
$invoice = $this->service->create([
'customer_id' => $customer->id,
'date' => '2026-03-06',
'due_date' => '2026-04-05',
'items' => [
['product_id' => $product->id, 'quantity' => 2, 'price' => 100],
],
]);
expect($invoice)
->toBeInstanceOf(Invoice::class)
->branch_id->toBe($this->branch->id)
->total->toBe(200.0)
->items->toHaveCount(1);
});
it('rejects invoice without items', function () {
$customer = Customer::factory()->create(['branch_id' => $this->branch->id]);
expect(fn () => $this->service->create([
'customer_id' => $customer->id,
'date' => '2026-03-06',
'due_date' => '2026-04-05',
'items' => [],
]))->toThrow(ValidationException::class);
});
it('rejects due_date before invoice date', function () {
$customer = Customer::factory()->create(['branch_id' => $this->branch->id]);
expect(fn () => $this->service->create([
'customer_id' => $customer->id,
'date' => '2026-03-06',
'due_date' => '2026-03-01',
'items' => [['product_id' => 1, 'quantity' => 1, 'price' => 50]],
]))->toThrow(ValidationException::class);
});
});
Multi-Tenant Isolation Tests
CRITICAL: Every feature must verify that Branch A cannot access Branch B data.
describe('Multi-Tenant Isolation', function () {
it('prevents cross-branch data access for invoices', function () {
$branchA = Branch::factory()->create();
$branchB = Branch::factory()->create();
$userA = User::factory()->for($branchA)->create();
$userB = User::factory()->for($branchB)->create();
$this->actingAs($userA);
$invoice = Invoice::factory()->create(['branch_id' => $branchA->id]);
$this->actingAs($userB);
$response = $this->getJson("/api/v1/invoices/{$invoice->id}");
$response->();
= ->();
->(, );
});
(, function () {
= ::()->();
= ::()->()->();
->();
= ::()->([ => ->id]);
= ->(, [
=> ->id,
=> ,
=> ,
=> [[ => , => , => ]],
]);
->();
(::()->branch_id)->(->id);
});
});
API Feature Test Pattern
describe('Invoice API', function () {
beforeEach(function () {
$this->branch = Branch::factory()->create();
$this->user = User::factory()->for($this->branch)->create();
$this->actingAs($this->user);
});
describe('GET /api/v1/invoices', function () {
it('returns paginated invoices for the current branch', function () {
Invoice::factory()->count(30)->create(['branch_id' => $this->branch->id]);
$response = $this->getJson('/api/v1/invoices?per_page=10');
$response
->assertOk()
->assertJsonCount(10, 'data')
->assertJsonPath('meta.total', 30);
});
it('filters by status', function () {
::()->([ => ->branch->id, => ]);
::()->([ => ->branch->id, => ]);
= ->();
->()->(, );
});
(, function () {
()->();
->()->();
});
});
(, function () {
(, function () {
= ->(, []);
->()
->([, , , ]);
});
});
});
Factory Pattern
class InvoiceFactory extends Factory
{
public function definition(): array
{
return [
'branch_id' => Branch::factory(),
'customer_id' => Customer::factory(),
'invoice_number' => $this->faker->unique()->numerify('INV-####'),
'date' => $this->faker->date(),
'due_date' => fn (array $attrs) => Carbon::parse($attrs['date'])->addDays(30),
'status' => 'draft',
'subtotal' => $this->faker->randomFloat(2, 100, 10000),
'tax' => fn (array ) => [] * ,
=> [] + [],
];
}
{
->(fn () => [ => , => ()]);
}
{
->(fn () => [
=> ,
=> ()->(),
]);
}
}
E2E Test Pattern (Cypress)
describe('Create Invoice', () => {
beforeEach(() => {
cy.login('sales@example.com');
cy.visit('/invoices/new');
});
it('creates a new invoice with line items', () => {
cy.get('[data-cy=customer-select]').click();
cy.get('[data-cy=customer-option]').first().click();
cy.get('[data-cy=invoice-date]').type('2026-03-06');
cy.get('[data-cy=due-date]').type('2026-04-05');
cy.get('[data-cy=add-item]').click();
cy.get('[data-cy=product-search]').type('Widget');
cy.get('[data-cy=product-option]').first().click();
cy.get('[data-cy=quantity]').clear().type('5');
cy.().(, );
cy.().(, );
cy.().();
cy.().(, );
cy.().(, );
});
(, {
cy.().();
cy.().();
cy.().(, );
});
});
Quality Gate Checklist
Every PR must pass these before merge:
Automated Gates
Manual Review Checklist
Test Data Principles
- Use factories — Never hardcode test data
- Isolate tests — Each test creates its own data
- Use
RefreshDatabase — Clean state per test
- Name descriptively — Test names explain the behavior, not the method
- One assertion per concept — Test one behavior, may use multiple
expect()
- Test edge cases: empty arrays, null values, max lengths, special characters
- Test authorization: verify both allowed and denied access
Bug Verification Pattern
When fixing a bug:
describe('Bug Fix: #167 — Tax calculation for exempt items', function () {
it('reproduces the original bug', function () {
$product = Product::factory()->create(['tax_exempt' => true, 'price' => 100]);
$tax = TaxService::calculate($product, quantity: 1);
expect($tax)->toBe(0.0);
});
it('still calculates tax for taxable items', function () {
$product = Product::factory()->create(['tax_exempt' => false, 'price' => 100]);
$tax = TaxService::calculate($product, quantity: 1);
expect($tax)->toBe(5.0);
});
});