Looking to build a robust Next.js SaaS application? Smoke testing is essential for quickly verifying your core functionality works after each deployment. This guide will show you how to implement smoke tests in your Next.js SaaS boilerplate or starter kit.
What is Smoke Testing?
Smoke testing verifies your application's critical features work after deployment. It's like a quick health check for your SaaS - run a few key tests to ensure nothing's broken before doing deeper testing.
For SaaS applications, smoke tests typically:
- Run quickly (usually under 5 minutes)
- Cover only the most critical user paths
- Verify core functionality works
- Run after every deployment
- Serve as an early warning system
Essential Smoke Test Scenarios for SaaS
1. Authentication Flow
test('basic auth flow', async ({ page }) => { const auth = new AuthPageObject(page); // Test sign up await auth.signUp({ email: 'smoke@example.com', password: 'smoketest123' }); await expect(page).toHaveURL('/dashboard'); // Test logout await auth.logout(); await expect(page).toHaveURL('/login'); // Test sign in await auth.signIn({ email: 'smoke@example.com', password: 'smoketest123' }); await expect(page).toHaveURL('/dashboard');});
2. Payment Integration
test('subscription flow', async ({ page }) => { const billing = new BillingPageObject(page); // Verify pricing page loads await billing.visitPricingPage(); await expect(page.locator('[data-test="pricing-plans"]')).toBeVisible(); // Test basic subscription flow with test card await billing.selectPlan('starter'); await billing.fillPaymentDetails({ card: '4242424242424242', exp: '12/25', cvc: '123' }); await expect(page.locator('[data-test="subscription-success"]')).toBeVisible();});
3. Core Feature Verification
test('core features', async ({ page }) => { const dashboard = new DashboardPageObject(page); // Create a new project await dashboard.createProject('Smoke Test Project'); await expect(page.locator('[data-test="project-title"]')) .toHaveText('Smoke Test Project'); // Verify basic CRUD operations await dashboard.addItem({ name: 'Test Item' }); await expect(dashboard.getItemsList()).toContainText('Test Item'); await dashboard.updateItem('Test Item', { name: 'Updated Item' }); await expect(dashboard.getItemsList()).toContainText('Updated Item'); await dashboard.deleteItem('Updated Item'); await expect(dashboard.getItemsList()).not.toContainText('Updated Item');});
Implementing Smoke Tests in Your CI Pipeline
GitHub Actions Configuration
name: Smoke Testson: deployment_status:jobs: smoke: if: github.event.deployment_status.state == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm install - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Run smoke tests run: npm run playwright test --grep "smoke" env: BASE_URL: ${{ github.event.deployment_status.target_url }} CI: true
Best Practices for Smoke Testing
1. Keep Tests Focused and Fast
Your smoke test suite should run in under 5 minutes. Focus on mission-critical paths:
- User authentication
- Payment processing
- Core feature accessibility
- Basic CRUD operations
- API health checks
2. Use Environment-Specific Test Data
// config/smoke.config.tsexport const smokeConfig = { testUser: { email: process.env.SMOKE_TEST_EMAIL || 'smoke@example.com', password: process.env.SMOKE_TEST_PASSWORD || 'smoketest123' }, testCard: { number: '4242424242424242', exp: '12/25', cvc: '123' }};
3. Implement Health Checks
test('api health check', async ({ request }) => { const response = await request.get('/api/health'); expect(response.ok()).toBeTruthy(); const json = await response.json(); expect(json.status).toBe('healthy'); expect(json.dependencies.database).toBe('connected'); expect(json.dependencies.cache).toBe('connected');});
4. Monitor and Alert
test.afterEach(async ({ }, testInfo) => { if (testInfo.status !== 'passed') { const { WebClient } = require('@slack/web-api'); const slack = new WebClient(process.env.SLACK_TOKEN); try { await slack.chat.postMessage({ channel: process.env.SLACK_CHANNEL, text: `🚨 Smoke test failed: ${testInfo.title}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*Smoke Test Failed*\n*Test:* ${testInfo.title}\n*Error:* ${testInfo.error}` } } ] }); } catch (error) { console.error('Failed to send Slack notification:', error); } }});
Simple Monitoring Integration
Here's a straightforward way to track your smoke test results:
// monitoring/results.tsinterface TestResult { name: string; duration: number; status: 'passed' | 'failed'; timestamp: number;}export async function reportTestResults(results: TestResult[]) { try { // Send to your error tracking service (e.g. Sentry, LogRocket) const failedTests = results.filter(test => test.status === 'failed'); if (failedTests.length > 0) { await notifyTeam(failedTests); } // Log to your existing monitoring solution console.log(JSON.stringify({ timestamp: Date.now(), tests: results, summary: { total: results.length, passed: results.filter(t => t.status === 'passed').length, failed: failedTests.length, avgDuration: results.reduce((acc, t) => acc + t.duration, 0) / results.length } })); } catch (error) { console.error('Failed to report results:', error); throw error; }}
Testing Environments: Where to Run Your Tests
To explain visually the flow of smoke testing, let's visualize it in a diagram:
Smoke Tests in Different Environments
- Staging Environment (Recommended Primary)
- Run full smoke test suite after every deployment
- Safe environment to test payment integrations
- Catches issues before they reach production
- Should mirror production as closely as possible
- Production Environment (Essential but Limited)
- Run minimal, non-destructive smoke tests
- Focus on read-only operations
- Avoid test data creation
- Never test payment flows
- Example production-safe tests:test('production health checks @prod', async ({ request }) => {// Check public API endpointsconst healthCheck = await request.get('/api/health');expect(healthCheck.ok()).toBeTruthy();// Verify critical pages loadconst marketing = await request.get('/');expect(marketing.ok()).toBeTruthy();// Check authentication endpoint availabilityconst auth = await request.get('/api/auth/csrf');expect(auth.ok()).toBeTruthy();});
E2E Tests in Production?
Production E2E testing is controversial but can be valuable when done right:
Benefits:
- Catches real-world issues
- Verifies actual user experiences
- Monitors true system performance
- Identifies third-party integration issues
Risks:
- Potential data corruption
- False positives from real user interactions
- Risk of triggering real transactions
- Privacy concerns with user data
Safe Production E2E Strategy:
// Configure separate test modesconst config = { production: { // Read-only operations only allowedOperations: ['GET', 'HEAD'], // Skip sensitive flows skipTests: ['payment', 'email', 'delete'], // Use dedicated test accounts testAccounts: { viewer: 'prod-test-viewer@company.com', // No admin test accounts in prod } }, staging: { // All operations allowed allowedOperations: ['GET', 'POST', 'PUT', 'DELETE'], skipTests: [], testAccounts: { admin: 'staging-admin@company.com', viewer: 'staging-viewer@company.com' } }};// Example production-safe testtest('critical user journey @prod', async ({ page }) => { const env = process.env.TEST_ENV || 'staging'; // Skip if test isn't production-safe test.skip( env === 'production' && !config[env].allowedOperations.includes('POST'), 'Test requires write operations' ); // Use environment-specific configuration const account = config[env].testAccounts.viewer; await page.login(account); // Verify critical path await expect(page.locator('dashboard')).toBeVisible(); await expect(page.locator('user-menu')).toContainText(account);});
Alternative: Health Checks
Instead of running full E2E tests in production, consider simple health checks:
// monitoring/health.tsexport async function checkHealth() { const criticalPaths = [ { path: '/api/health', name: 'API Health' }, { path: '/api/auth/session', name: 'Auth Service' }, { path: '/pricing', name: 'Pricing Page' } ]; for (const { path, name } of criticalPaths) { try { const start = Date.now(); const response = await fetch(path); const duration = Date.now() - start; if (!response.ok) { throw new Error(`${name} returned ${response.status}`); } console.log(`✅ ${name}: ${duration}ms`); } catch (error) { console.error(`❌ ${name} failed:`, error); await notifyTeam({ service: name, error: error.message, timestamp: new Date().toISOString() }); } }}
This approach provides:
- Continuous uptime monitoring
- Performance tracking
- Early warning system
- No risk of data corruption
- Real-world latency metrics
Recommended Testing Strategy
- Staging Environment:
- Full smoke test suite
- Complete E2E test coverage
- Payment integration testing
- Data modification tests
- Production Environment:
- Minimal, read-only smoke tests
- Synthetic monitoring
- Uptime checks
- Performance monitoring
- Limited, safe E2E tests
This balanced approach gives you confidence in your deployments while protecting your production environment.
Common Pitfalls to Avoid
- Too Many Tests
- Keep your smoke test suite minimal
- Focus on business-critical paths
- Move detailed tests to your full E2E suite
- Unreliable Test Data
- Use dedicated test accounts
- Reset test data before each run
- Never rely on production data
- Missing Error Handling
- Implement proper error reporting
- Set up alerting for failures
- Track test metrics over time
Smoke Testing vs. E2E Testing
While our previous article covered comprehensive E2E testing, smoke testing serves a different purpose:
Smoke Tests:
- Scope: Critical paths only
- Duration: Less than 5 minutes
- Frequency: Every deployment
- Coverage: 10-20% of functionality
- Purpose: Quick verification
E2E Tests:
- Scope: Comprehensive
- Duration: 15-60 minutes
- Frequency: Daily/Weekly
- Coverage: 70-90% of functionality
- Purpose: Deep validation
Conclusion
Smoke testing is your first line of defense against major issues in your SaaS application. By implementing a focused set of smoke tests, you can:
- Catch critical issues quickly
- Reduce deployment risks
- Maintain continuous delivery
- Build confidence in your deployment process
Remember, the goal of smoke testing isn't to catch every possible issue - it's to quickly verify that your application's core functionality works as expected. Combined with comprehensive E2E testing, smoke tests form a crucial part of your quality assurance strategy.
Start with the essential scenarios outlined in this guide, and gradually expand your smoke test suite based on your application's specific needs. The time invested in setting up proper smoke tests will pay off in reduced downtime, faster issue detection, and more confident deployments.
About Makerkit
Looking for a production-ready SaaS starter kit with built-in testing infrastructure? Check out Makerkit, a Next.js SaaS starter kit that comes with pre-configured smoke tests and E2E testing setup.