Overview
SushiGo employs a comprehensive testing strategy across three layers:
Backend (API) : PHPUnit for unit and feature tests
Frontend (Webapp) : Vitest for component and unit tests
E2E : Cypress for full-stack integration tests
Backend Testing (PHPUnit)
The Laravel API uses PHPUnit with a dedicated test database and transactional rollback for isolation.
Test Configuration
Configuration is defined in code/api/phpunit.xml:
< phpunit >
< testsuites >
< testsuite name = "Unit" >
< directory > tests/Unit </ directory >
</ testsuite >
< testsuite name = "Feature" >
< directory > tests/Feature </ directory >
</ testsuite >
</ testsuites >
< php >
< env name = "APP_ENV" value = "testing" />
< env name = "DB_CONNECTION" value = "pgsql" />
< env name = "DB_DATABASE" value = "mydb_test" />
< env name = "CACHE_STORE" value = "array" />
< env name = "QUEUE_CONNECTION" value = "sync" />
</ php >
</ phpunit >
Tests use a separate mydb_test database created during container initialization.
Running API Tests
All Tests
Specific Test Suite
Specific Test File
With Coverage
# Inside the container (recommended)
docker exec -it dev_container bash -c "cd /app/code/api && php artisan test"
# Or from VS Code terminal (already in container)
cd /app/code/api
php artisan test
Test Structure
code/api/tests/
├── Unit/ # Isolated unit tests (models, services)
│ ├── Models/
│ │ └── EmployeeTest.php
│ └── Services/
│ └── WageCalculatorTest.php
└── Feature/ # Integration tests (controllers, API endpoints)
├── Auth/
│ └── LoginTest.php
├── Employees/
│ ├── CreateEmployeeTest.php
│ └── ListEmployeesTest.php
└── Inventory/
└── StockMovementTest.php
Writing Backend Tests
Feature Test Example
<? php
namespace Tests\Feature\Employees ;
use Tests\ TestCase ;
use App\Models\ User ;
use Illuminate\Foundation\Testing\ RefreshDatabase ;
class CreateEmployeeTest extends TestCase
{
use RefreshDatabase ;
public function test_admin_can_create_employee () : void
{
$admin = User :: factory () -> create ();
$admin -> assignRole ( 'admin' );
$response = $this -> actingAs ( $admin , 'api' )
-> postJson ( '/api/v1/employees' , [
'name' => 'John Doe' ,
'email' => '[email protected] ' ,
'role' => 'cook' ,
]);
$response -> assertStatus ( 201 )
-> assertJsonStructure ([
'data' => [ 'id' , 'name' , 'email' , 'role' ],
]);
$this -> assertDatabaseHas ( 'employees' , [
'email' => '[email protected] ' ,
]);
}
}
Unit Test Example
<? php
namespace Tests\Unit\Services ;
use Tests\ TestCase ;
use App\Services\ WageCalculator ;
class WageCalculatorTest extends TestCase
{
public function test_calculates_monthly_wage_correctly () : void
{
$calculator = new WageCalculator ();
$result = $calculator -> calculateMonthly (
dailyWage : 200 ,
daysWorked : 22
);
$this -> assertEquals ( 4400 , $result );
}
}
Test Database
The test database is automatically created during container initialization.
Manual Creation:
docker exec -it dev_container psql -h pgsql -U admin -d mydb \
-c "CREATE DATABASE mydb_test;"
Resetting Test Database:
# Migrations run automatically with RefreshDatabase trait
# Manual reset if needed:
docker exec -it dev_container bash -c "cd /app/code/api && php artisan migrate:fresh --env=testing"
Never run migrate:fresh on production or the main mydb database!
Frontend Testing (Vitest)
The React webapp uses Vitest for fast unit and component testing.
Test Configuration
Configuration in code/webapp/vite.config.ts and code/webapp/vitest.config.ts.
Running Frontend Tests
Run Tests
With Coverage
Specific Tests
# Inside container or on host (if Node installed)
cd code/webapp
# Run all tests
npm run test
# Watch mode (re-runs on file changes)
npm run test -- --watch
Test Structure
code/webapp/
├── src/
│ ├── components/
│ │ └── ui/
│ │ ├── button.tsx
│ │ └── button.test.tsx
│ ├── services/
│ │ └── employee-api.test.ts
│ └── lib/
│ └── utils.test.ts
└── vitest.config.ts
Writing Frontend Tests
Component Test Example
import { describe , it , expect } from 'vitest'
import { render , screen } from '@testing-library/react'
import { Button } from './button'
describe ( 'Button' , () => {
it ( 'renders with correct text' , () => {
render ( < Button > Click me </ Button > )
expect ( screen . getByText ( 'Click me' )). toBeInTheDocument ()
})
it ( 'applies variant classes' , () => {
render ( < Button variant = "destructive" > Delete </ Button > )
const button = screen . getByText ( 'Delete' )
expect ( button ). toHaveClass ( 'bg-destructive' )
})
})
API Service Test Example
import { describe , it , expect , vi } from 'vitest'
import { employeeApi } from './employee-api'
describe ( 'employeeApi' , () => {
it ( 'fetches employees list' , async () => {
const mockData = {
data: {
employees: [
{ id: '1' , name: 'John Doe' },
],
},
}
global . fetch = vi . fn (() =>
Promise . resolve ({
json : () => Promise . resolve ( mockData ),
})
)
const result = await employeeApi . list ()
expect ( result . employees ). toHaveLength ( 1 )
expect ( result . employees [ 0 ]. name ). toBe ( 'John Doe' )
})
})
End-to-End Testing (Cypress)
Cypress tests simulate real user workflows across the entire stack.
E2E Environment
E2E tests run in an isolated environment with:
Separate database (mydb_e2e)
Dedicated container (e2e_container)
Fresh seeded data on each run
Running E2E Tests
Interactive Mode (Recommended)
Headless Mode
Direct (without Docker)
# Start Cypress UI with VNC viewer
make cypress-ui
# Open browser to http://localhost:6080
# Click "Open Cypress" in the desktop
E2E Test Structure
code/webapp/cypress/
├── e2e/ # Test files
│ ├── auth/
│ │ ├── login.cy.ts
│ │ └── logout.cy.ts
│ ├── employees/
│ │ ├── create-employee.cy.ts
│ │ └── list-employees.cy.ts
│ └── inventory/
│ └── stock-movement.cy.ts
├── fixtures/ # Test data
│ └── users.json
├── support/
│ ├── commands.ts # Custom commands
│ └── e2e.ts # Global config
└── cypress.config.ts # Cypress configuration
Writing E2E Tests
// cypress/e2e/employees/create-employee.cy.ts
describe ( 'Create Employee' , () => {
beforeEach (() => {
cy . login ( '[email protected] ' , 'admin123456' )
cy . visit ( '/employees' )
})
it ( 'creates a new employee successfully' , () => {
cy . get ( '[data-testid="create-employee-btn"]' ). click ()
cy . get ( '[name="name"]' ). type ( 'Jane Smith' )
cy . get ( '[name="email"]' ). type ( '[email protected] ' )
cy . get ( '[name="role"]' ). select ( 'cook' )
cy . get ( '[data-testid="submit-btn"]' ). click ()
cy . contains ( 'Employee created successfully' ). should ( 'be.visible' )
cy . contains ( 'Jane Smith' ). should ( 'be.visible' )
})
it ( 'shows validation errors for invalid input' , () => {
cy . get ( '[data-testid="create-employee-btn"]' ). click ()
cy . get ( '[data-testid="submit-btn"]' ). click ()
cy . contains ( 'Name is required' ). should ( 'be.visible' )
cy . contains ( 'Email is required' ). should ( 'be.visible' )
})
})
Custom Cypress Commands
Define reusable commands in cypress/support/commands.ts:
Cypress . Commands . add ( 'login' , ( email : string , password : string ) => {
cy . session ([ email , password ], () => {
cy . visit ( '/login' )
cy . get ( '[name="email"]' ). type ( email )
cy . get ( '[name="password"]' ). type ( password )
cy . get ( '[type="submit"]' ). click ()
cy . url (). should ( 'not.include' , '/login' )
})
})
declare global {
namespace Cypress {
interface Chainable {
login ( email : string , password : string ) : Chainable < void >
}
}
}
Cypress Configuration
Base URL and environment variables in code/webapp/cypress.config.ts:
export default defineConfig ({
e2e: {
baseUrl: 'https://sushigo.e2e.local' ,
video: true ,
screenshotOnRunFailure: true ,
} ,
})
Override via environment variables:
CYPRESS_baseUrl = https://devtest.sushigo.local make cypress-run
Continuous Integration
GitHub Actions Example
name : Tests
on : [ push , pull_request ]
jobs :
backend :
runs-on : ubuntu-latest
services :
postgres :
image : postgres:15
env :
POSTGRES_DB : mydb_test
POSTGRES_USER : admin
POSTGRES_PASSWORD : admin
ports :
- 5432:5432
steps :
- uses : actions/checkout@v3
- name : Install dependencies
run : cd code/api && composer install
- name : Run tests
run : cd code/api && php artisan test
frontend :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- name : Install dependencies
run : cd code/webapp && npm ci
- name : Run tests
run : cd code/webapp && npm run test
e2e :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- name : Start services
run : docker compose up -d
- name : Run Cypress
run : docker compose -f docker-compose.e2e.yml run --rm cypress
Test Data Management
Seeders for Testing
Use RepeatableSeeder for test data that resets on each run:
// database/seeders/Testing/TestDataSeeder.php
class TestDataSeeder extends RepeatableSeeder
{
public function run () : void
{
User :: factory () -> create ([
'email' => '[email protected] ' ,
'password' => Hash :: make ( 'password' ),
]);
Employee :: factory ( 10 ) -> create ();
}
}
Fixtures for Cypress
Use JSON fixtures for consistent test data:
// cypress/fixtures/users.json
{
"admin" : {
"email" : "[email protected] " ,
"password" : "admin123456"
},
"manager" : {
"email" : "[email protected] " ,
"password" : "inventory123456"
}
}
// Use in tests
cy . fixture ( 'users' ). then (( users ) => {
cy . login ( users . admin . email , users . admin . password )
})
Debugging Tests
PHPUnit Debugging
// Add breakpoint in test
dd ( $response -> json ());
// Print variable
dump ( $employee -> toArray ());
// Enable verbose output
php artisan test -- verbose
Vitest Debugging
// Console log in test
console . log ( 'Result:' , result )
// Debug mode
npm run test -- -- reporter = verbose
// Run single test in watch mode
npm run test -- -- watch -- grep = "specific test"
Cypress Debugging
// Pause test execution
cy . pause ()
// Print value
cy . get ( '[name="email"]' ). then (( $el ) => {
console . log ( $el . val ())
})
// Take screenshot
cy . screenshot ( 'debug-screen' )
// View in interactive mode
make cypress - ui
Open Cypress UI at http://localhost:6080 to see tests running in real-time with time-travel debugging.
Best Practices
Use RefreshDatabase trait in Laravel tests
Clear cache/state between Vitest tests
Seed E2E database fresh on each Cypress run
Never depend on test execution order
// ✅ GOOD - Descriptive test names
test_admin_can_create_employee_with_valid_data ()
test_validates_required_fields ()
test_returns_404_for_missing_employee ()
// ❌ BAD - Vague names
test_create ()
test_validation ()
test_api ()
public function test_example () : void
{
// Arrange - Set up test data
$user = User :: factory () -> create ();
// Act - Perform action
$response = $this -> actingAs ( $user ) -> get ( '/api/employees' );
// Assert - Verify result
$response -> assertStatus ( 200 );
}
// Mock HTTP calls
Http :: fake ([
'api.example.com/*' => Http :: response ([ 'data' => 'mocked' ], 200 ),
]);
// Mock time
Carbon :: setTestNow ( '2026-03-06 10:00:00' );
Troubleshooting
# Ensure test database exists
docker exec -it postgres_container psql -U admin -l | grep mydb_test
# Create if missing
docker exec -it dev_container psql -h pgsql -U admin -d mydb \
-c "CREATE DATABASE mydb_test;"
# Run migrations
docker exec -it dev_container bash -c "cd /app/code/api && php artisan migrate --env=testing"
# Clear node_modules and reinstall
cd code/webapp
rm -rf node_modules package-lock.json
npm install
# Ensure Vitest is installed
npm install -D vitest @vitest/coverage-v8
Cypress connection refused
# Ensure E2E container is running
docker compose ps test_e2e
# Start if not running
make e2e-up
# Check baseUrl is correct
docker compose -f docker-compose.e2e.yml logs test_e2e
# Verify hosts file has sushigo.e2e.local
cat /etc/hosts | grep sushigo
Cypress UI VNC not loading
# Check cypress-ui container is running
docker compose ps cypress-ui
# View logs
docker compose -f docker-compose.e2e.yml logs cypress-ui
# Rebuild container
docker compose -f docker-compose.e2e.yml build cypress-ui
docker compose -f docker-compose.e2e.yml up -d cypress-ui
# Access at http://localhost:6080
Next Steps
Conventions Learn code and Git conventions
Docker Compose Understand the container architecture