Testing Overview
SaaS4Builders ships with a comprehensive test suite covering both the Laravel backend and the Nuxt frontend. Tests mirror the application's layered architecture — feature tests validate HTTP endpoints end-to-end, unit tests verify individual actions and queries, and frontend tests cover stores, composables, components, and API modules. Everything runs in Docker, and a GitHub Actions CI pipeline enforces quality on every push.
Testing Philosophy
Three principles guide the testing strategy across both stacks:
1. Tests Mirror Application Architecture
Backend tests follow the domain-driven structure — feature tests live in tests/Feature/Api/V1/ matching the controller hierarchy, and unit tests live in tests/Unit/Application/ matching the action/query structure. Frontend tests are colocated with their features in __tests__/ directories inside each vertical slice.
2. External Services Are Always Mocked
Stripe, OAuth providers, and any external API are mocked in tests. The backend uses Mockery to mock domain contracts (interfaces), so tests never make real HTTP calls. The frontend uses Vitest's vi.mock() to replace API client modules with controlled responses.
3. Tenant Isolation Is a First-Class Concern
Multi-tenant data isolation is one of the most critical invariants in the application. Dedicated test helpers (WithTenantContext trait on the backend, mock factories on the frontend) make it straightforward to set up tenant contexts and verify that tenant A never sees tenant B's data. See Testing Multi-Tenant Isolation for a deep dive.
phpunit.xml). The CI pipeline uses PostgreSQL 16 for production parity. If you encounter database-specific test failures locally that pass in CI (or vice versa), check which database engine your tests are targeting.Toolchain at a Glance
| Concern | Backend | Frontend |
|---|---|---|
| Test framework | PHPUnit v12 | Vitest 3.2 |
| Mocking | Mockery | vi.mock() / vi.hoisted() |
| Static analysis | PHPStan level 9 | TypeScript strict mode |
| Code formatting | Laravel Pint | ESLint |
| Test utilities | WithTenantContext trait | @nuxt/test-utils + @vue/test-utils |
| DOM environment | N/A | happy-dom |
| Coverage provider | Xdebug (CI) | v8 |
| CI runner | GitHub Actions (PHP 8.3 + PostgreSQL 16 + Redis 7) | GitHub Actions (Node 20) |
Quick Start Commands
All commands run from the project root via make or directly through Docker:
| Goal | Make Command | Docker Equivalent |
|---|---|---|
| Run all tests (both stacks) | make test | — |
| Backend tests only | make test-back | docker compose exec php php artisan test |
| Frontend tests only | make test-front | docker compose exec node pnpm test |
| Filter a single backend test | — | docker compose exec php php artisan test --filter=test_user_can_only_access_their_tenant |
| Run a single frontend test file | — | docker compose exec node pnpm test features/core/docs/billing/composables/__tests__/useSubscription.test.ts |
| Backend tests with coverage | — | docker compose exec php php artisan test --coverage |
| Frontend tests with coverage | — | docker compose exec node pnpm test --run --coverage |
| All linting (both stacks) | make lint | — |
| Full CI simulation | make check | make lint && make test |
docker compose exec php or docker compose exec node to run commands inside the containers. Running php artisan test or pnpm test outside Docker will use your host's PHP/Node versions, which may differ from the project's requirements.Test Directory Structure
Backend
backend/tests/
├── Feature/
│ ├── Api/V1/ # HTTP endpoint tests
│ │ ├── Admin/ # Platform admin endpoints
│ │ │ ├── Billing/ # Stripe price management
│ │ │ ├── Catalog/ # Products, plans, features
│ │ │ ├── Impersonation/
│ │ │ ├── Tenant/ # Admin tenant management
│ │ │ └── ...
│ │ ├── Auth/ # Authentication endpoints
│ │ ├── Profile/ # User profile
│ │ └── Tenant/ # Tenant-scoped endpoints
│ ├── Auth/ # Auth flow tests (login, register, OAuth)
│ ├── Tenancy/ # Tenant isolation & resolution
│ ├── Webhooks/ # Stripe webhook signature verification
│ └── Database/Seeders/ # Seeder tests
├── Unit/
│ └── Application/ # Action & Query unit tests
│ ├── Auth/Actions/
│ └── ...
├── Traits/
│ └── WithTenantContext.php # Tenant test helper trait
└── TestCase.php # Base test class
Frontend
frontend/
├── features/
│ ├── foundation/
│ │ ├── auth/stores/__tests__/ # Auth store tests
│ │ ├── currencies/api/__tests__/ # Currency API tests
│ │ ├── entitlements/api/__tests__/ # Entitlements tests
│ │ └── tenancy/stores/__tests__/ # Tenant store tests
│ ├── core/
│ │ ├── billing/
│ │ │ ├── api/__tests__/ # Billing API module tests
│ │ │ ├── composables/__tests__/ # Subscription composable tests
│ │ │ ├── components/__tests__/ # UI component tests
│ │ │ └── __tests__/ # Schema validation tests
│ │ ├── catalog/ # Catalog feature tests
│ │ └── team/ # Team feature tests
│ └── product/ # Product feature tests
└── tests/helpers/
├── mockSession.ts # User, tenant, session factories
├── mockCatalog.ts # Product, plan, feature factories
└── mockTeam.ts # Team member, invitation factories
The key principle: backend tests mirror the application architecture (tests/Feature/Api/V1/ matches Http/Controllers/Api/V1/), while frontend tests are colocated with the feature they test (features/core/docs/billing/composables/__tests__/ sits next to features/core/docs/billing/composables/).
Test Configuration
Backend — phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
<env name="STRIPE_KEY" value="pk_test_dummy"/>
<env name="STRIPE_SECRET" value="sk_test_dummy"/>
<env name="STRIPE_WEBHOOK_SECRET" value="whsec_dummy"/>
</php>
</phpunit>
Key settings: SQLite in-memory for speed, array drivers for cache/mail/session/queue (no external services), dummy Stripe keys (all Stripe calls are mocked), and low bcrypt rounds for faster test execution.
Frontend — vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
// Ensure nuxt-studio module doesn't throw during test Nuxt initialization
process.env.STUDIO_REPOSITORY_OWNER ??= 'test'
process.env.STUDIO_REPOSITORY_NAME ??= 'test'
export default defineVitestConfig({
test: {
environment: 'nuxt',
environmentOptions: {
nuxt: {
domEnvironment: 'happy-dom',
},
},
include: ['**/*.{test,spec}.{js,ts,vue}'],
exclude: ['node_modules', '.nuxt', '.output', 'dist'],
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/**',
'.nuxt/**',
'.output/**',
'dist/**',
'**/*.d.ts',
'**/*.config.*',
'**/types/**',
],
},
},
})
Key settings: defineVitestConfig from @nuxt/test-utils/config sets up the Nuxt-aware test environment automatically. happy-dom is the DOM implementation (lighter than jsdom). globals: true means you do not need to import describe, it, and expect in every test file.
CI Pipeline
The project includes a GitHub Actions workflow (.github/workflows/ci.yml) that runs on every push and pull request to main and develop. It consists of three parallel jobs:
| Job | What It Does | Services |
|---|---|---|
| Backend | Composer install, migrate, Pint (code style check), PHPStan (static analysis), parallel tests with coverage, Codecov upload | PostgreSQL 16, Redis 7 |
| Frontend | pnpm install (frozen lockfile), ESLint, TypeScript check, Vitest with coverage, production build | None |
| Security | composer audit for PHP dependencies, pnpm audit for Node dependencies | None |
The backend job runs against PHP 8.3 with PostgreSQL 16 and Redis 7 as service containers. The frontend job runs on Node 20 with pnpm 9. Both jobs use dependency caching for faster runs.
pnpm build). This catches issues that might not surface during development — such as missing imports, SSR-incompatible code, or type errors that only appear during production builds.What's Next
- Backend Testing (PHPUnit) — Base TestCase,
WithTenantContexttrait, model factories, Stripe mocking patterns, and test conventions - Frontend Testing (Vitest) — Vitest configuration, Nuxt test utilities, mock factories, store/composable/component test patterns
Frontend i18n
Nuxt i18n module setup, JSON locale files, translation usage in components, Zod validation integration, locale-aware currency formatting, and how to extend the system.
Backend Testing (PHPUnit)
How to write backend tests: base TestCase, WithTenantContext trait, model factories, Stripe mocking patterns, feature and unit test conventions, and running tests.