Simplicity first: Testing should be simple to write, simple to maintain, and simple to understand. Tests should verify behavior, not implementation.
Startup agility: Write tests that provide confidence without slowing down development. Focus on critical paths first. Add comprehensive coverage when you have real problems or scale.
- Unit Tests: Fast, isolated tests for business logic and utilities
- Integration Tests: Test API endpoints and database interactions
- End-to-End Tests: Test critical user flows (use sparingly)
- Manual Smoke Tests: Quick verification of core functionality
- Test the happy path first
- Test critical error cases
- Test business logic thoroughly
- Don't test framework code or trivial getters/setters
- Focus on what could break, not what can't
- Aim for meaningful coverage, not 100%
- Focus on critical paths and business logic
- Use coverage to find untested code, not as a goal
- Remove tests that don't add value
- Business logic and domain rules
- Utility functions and helpers
- Complex calculations or transformations
- Error handling logic
- Edge cases in business rules
- Framework code (Laravel, Vue, Inertia.js)
- Simple getters/setters
- Trivial functions with no logic
- Third-party library code
- Database schema (tested via integration tests)
- Keep tests simple and readable
- One assertion per test when possible
- Use descriptive test names that explain what they verify
- Test behavior, not implementation
- Mock external dependencies (database, APIs, services)
- Keep tests fast (run in milliseconds)
- PHPUnit: Default testing framework (built into Laravel)
- Laravel Testing Utilities: TestCase, factories, assertions
- Database Factories: For generating test data
- Vitest: Fast unit test framework (Vite-based, optional)
- @vue/test-utils: Vue component testing utilities (optional)
- Laravel Feature Tests: Test Inertia pages (recommended)
// Laravel service example
class OrganizationServiceTest extends TestCase
{
public function test_creates_organization_with_valid_name(): void
{
// Arrange
$user = User::factory()->create();
$name = 'My Organization';
$service = new OrganizationService();
// Act
$result = $service->create($user->id, $name);
// Assert
$this->assertEquals($name, $result->name);
$this->assertEquals($user->id, $result->user_id);
}
public function test_throws_exception_when_name_is_empty(): void
{
// Arrange
$user = User::factory()->create();
$name = '';
$service = new OrganizationService();
// Act & Assert
$this->expectException(ValidationException::class);
$service->create($user->id, $name);
}
}- API endpoint behavior (request/response)
- Database interactions and queries
- Authentication and authorization
- Request validation
- Error responses
- Multi-tenant isolation (organization scoping)
- Test against real database (test database, not production)
- Use test fixtures for consistent test data
- Clean up test data after tests
- Test complete request/response cycles
- Verify database state after operations
- Test error scenarios (400, 401, 403, 500)
- PHPUnit: Test framework
- Laravel HTTP Testing: Built-in HTTP test methods
- Laravel Feature Tests: Full request/response testing
- Database Transactions: Automatic rollback in tests
// Laravel feature test example
class OrganizationsControllerTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_create_organization(): void
{
// Arrange
$user = User::factory()->create();
// Act
$response = $this->actingAs($user)
->post('/api/organizations', [
'name' => 'My Organization',
]);
// Assert
$response->assertStatus(201);
$this->assertDatabaseHas('organizations', [
'name' => 'My Organization',
'user_id' => $user->id,
]);
}
}const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
// Get auth token for test user
authToken = await getTestAuthToken();
});
afterAll(async () => { await app.close(); // Clean up test database });
describe('POST /api/organizations', () => {
it('should create organization with valid name', () => {
return request(app.getHttpServer())
.post('/api/organizations')
.set('Authorization', Bearer ${authToken})
.send({ name: 'Test Organization' })
.expect(201)
.expect((res) => {
expect(res.body.data.name).toBe('Test Organization');
expect(res.body.data.orgId).toBeDefined();
});
});
it('should return 401 without authentication', () => {
return request(app.getHttpServer())
.post('/api/organizations')
.send({ name: 'Test Organization' })
.expect(401);
});
}); });
## End-to-End / UI Testing
### 1. What to Test
- Critical user flows (login, create organization, add AWS account)
- Multi-step processes
- Cross-page navigation
- Form submissions and validations
- Error handling in UI
### 2. When to Use E2E Tests
- Critical user journeys that must work
- Complex multi-step workflows
- Features with high business impact
- Don't test every page - focus on critical paths
### 3. Tools and Frameworks
#### Playwright (Recommended)
- **Why Playwright**:
- Fast and reliable
- Cross-browser testing (Chrome, Firefox, Safari)
- Great debugging tools
- Auto-waiting for elements
- Screenshot and video recording
- Good TypeScript support
#### Alternative: Cypress
- Good for component testing
- Real browser testing
- Good developer experience
- Slower than Playwright for E2E
### 4. Playwright Setup and Structure
```typescript
// tests/e2e/organizations.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Organization Management', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login');
await page.fill('input[type="email"]', 'test@example.com');
await page.fill('input[type="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('should create new organization', async ({ page }) => {
// Navigate to organizations page
await page.click('text=Organizations');
await page.waitForURL('/organizations');
// Click add organization button
await page.click('text=Add Organization');
// Fill form
await page.fill('input[id="orgName"]', 'My New Organization');
await page.click('button:has-text("Create")');
// Verify organization appears in list
await expect(page.locator('text=My New Organization')).toBeVisible();
});
test('should switch between organizations', async ({ page }) => {
// Select organization from dropdown
await page.click('[data-testid="organization-selector"]');
await page.click('text=Organization 2');
// Verify UI updates
await expect(page.locator('text=Organization 2')).toBeVisible();
});
});
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});- Quick verification that core functionality works
- Catch obvious regressions before deployment
- Verify critical paths after major changes
- Not a replacement for automated tests
- Before deploying to production
- After major feature changes
- After infrastructure changes
- Weekly or before important releases
- Can register new user
- Can login with email/password
- Can login with Google OAuth
- Can logout
- Protected routes redirect to login when not authenticated
- Can view organizations list
- Can create new organization
- Can switch between organizations
- Can update organization name
- Can delete organization (when no AWS accounts)
- Can initiate AWS account addition
- CloudFormation URL opens correctly
- Can view AWS accounts list
- Account status displays correctly
- Can start a scan
- Can view scan status
- Can view scan results
- Can view reports
- Can export report (JSON)
- Dashboard displays correctly
- Create test checklist for current features
- Perform tests manually in staging environment
- Document any issues found
- Fix critical issues before deployment
- Update checklist as features are added
backend/
├── src/
│ └── [module]/
│ ├── [module].service.spec.ts # Unit tests
│ └── [module].controller.spec.ts # Unit tests
├── test/
│ └── [module].e2e-spec.ts # Integration tests
└── jest.config.js
frontend/
├── components/
│ └── [component].spec.ts # Component unit tests
├── stores/
│ └── [store].spec.ts # Store unit tests
├── tests/
│ └── e2e/
│ └── [feature].spec.ts # E2E tests
└── vitest.config.ts
- Unit tests:
*.spec.tsor*.test.ts - Integration tests:
*.e2e-spec.ts - E2E tests:
*.spec.tsintests/e2e/ - Use descriptive test names:
should create organization when name is valid
- Create reusable test data factories
- Use factories instead of hardcoded data
- Keep fixtures simple and focused
- Clean up test data after tests
- Use separate test database
- Reset database between test runs (or use transactions)
- Use migrations for test database schema
- Don't use production data in tests
- Mock external services (AWS SDK, Firebase)
- Mock database in unit tests
- Use real database in integration tests
- Mock API calls in frontend unit tests
- Run unit tests on every commit
- Run integration tests on pull requests
- Run E2E tests before merging to main
- Fail build if tests fail
- Generate coverage reports
- Run tests in parallel when possible
- Cache dependencies to speed up runs
- Use test matrix for multiple environments
- Set reasonable timeouts
- Bugs Found: When bugs are found in production, add tests to prevent regression
- Complex Logic: When business logic becomes complex, add more tests
- Critical Features: When features are critical to business, ensure thorough testing
- User Reports: When users report issues, add tests to verify fixes
- Identify Gaps: Use coverage reports to find untested code
- Prioritize: Focus on critical paths and business logic
- Add Incrementally: Add tests as you work on features
- Maintain: Keep tests updated as code changes
- Don't aim for 100% coverage (aim for meaningful coverage)
- Don't test framework code
- Don't write tests that test the test framework
- Don't add tests "just in case" - add them when needed
- Don't skip tests to move faster (they'll slow you down later)
- After Profiling: Only test performance of code that profiling shows is slow
- User Impact: When performance issues affect real users
- Scale: When you have significantly more users/data
- Business Impact: When performance affects business metrics
- Measure Baseline: Establish performance baselines
- Identify Bottlenecks: Use profiling to find slow code
- Test Incrementally: Test one optimization at a time
- Validate Impact: Measure after each optimization
- Document Why: Record why performance tests were needed
- Don't performance test code that isn't a bottleneck
- Don't performance test without profiling first
- Don't optimize for hypothetical scale
- Don't add performance tests "just in case"
- Tests should be easy to read and understand
- Remove tests that don't add value
- Refactor tests when they become complex
- Keep test setup minimal
- Update tests when code changes
- Remove tests for removed features
- Fix flaky tests immediately
- Don't ignore test failures
- Document complex test scenarios
- Explain why tests exist (link to bugs/requirements)
- Keep test data factories documented
- Document test environment setup
- Writing tests that test the test framework
- Aiming for 100% coverage instead of meaningful coverage
- Testing framework code or trivial functions
- Skipping tests to move faster
- Ignoring flaky tests
- Writing tests that are hard to understand
- Testing implementation instead of behavior
- Adding performance tests without profiling first
- Testing for hypothetical problems
- Complex test setups that are hard to maintain
- PHPUnit: Unit and integration testing (built into Laravel)
- Laravel HTTP Testing: API endpoint testing
- Laravel Testing Utilities: TestCase, factories, assertions
- Database Factories: Generate test data
- Vitest: Unit testing (fast, Vite-based, optional)
- @vue/test-utils: Vue component testing (optional)
- Laravel Feature Tests: Test Inertia pages (recommended)
- Playwright: End-to-end UI testing (when needed)
- Laravel Database Factories: Generate test data
- RefreshDatabase Trait: Automatic database cleanup
- MSW (Mock Service Worker): API mocking in frontend tests (if needed)
Before considering tests complete:
- Tests verify behavior, not implementation
- Tests are readable and maintainable
- Critical paths are tested
- Error cases are tested
- Tests run fast (unit tests < 1s each)
- Tests are reliable (not flaky)
- Test data is properly managed
- Tests are updated when code changes