Production Kanban: Testing, Deployment & Scale
Shipping a production kanban board requires comprehensive testing, automated deployment, and infrastructure that scales to thousands of concurrent users. A robust CI/CD pipeline catches regressions; E2E tests validate user workflows; load testing ensures the server and frontend handle traffic spikes. Production kanban apps (Trello, Linear) are deployed globally with 99.95% uptime SLAs and sub-100 ms response times.
Setting Up Testing Infrastructure
Start with unit and integration tests using React Testing Library:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest
import { render, screen, fireEvent } from '@testing-library/react';
import { Board } from './Board';
describe('Kanban Board', () => {
test('renders columns and tasks', () => {
render(<Board />);
expect(screen.getByText('To Do')).toBeInTheDocument();
expect(screen.getByText('In Progress')).toBeInTheDocument();
expect(screen.getByText('Fix button click')).toBeInTheDocument();
});
test('moves task between columns on drag', async () => {
const { container } = render(<Board />);
const task = screen.getByText('Fix button click');
const inProgressColumn = screen.getByText('In Progress').parentElement;
// Simulate drag and drop
fireEvent.dragStart(task);
fireEvent.dragEnter(inProgressColumn);
fireEvent.dragOver(inProgressColumn);
fireEvent.drop(inProgressColumn);
// Task should now be in In Progress
expect(inProgressColumn).toContainElement(task);
});
test('persists tasks to localStorage', () => {
render(<Board />);
const task = screen.getByText('Fix button click');
fireEvent.dragStart(task);
fireEvent.drop(screen.getByText('In Progress').parentElement);
// Check localStorage
const saved = JSON.parse(localStorage.getItem('kanban-tasks'));
expect(saved.inProgress).toContainEqual(
expect.objectContaining({ title: 'Fix button click' })
);
});
});
These tests verify core functionality: rendering, drag-and-drop, and persistence. Use @testing-library/user-event for more realistic user interactions.
End-to-End Testing with Playwright
E2E tests validate entire user workflows:
npm install --save-dev @playwright/test
// kanban.e2e.test.js
import { test, expect } from '@playwright/test';
test.describe('Kanban Board E2E', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000');
});
test('user can create, move, and complete tasks', async ({ page }) => {
// Create a task
await page.click('button:has-text("New Task")');
await page.fill('input[placeholder="Task title"]', 'Deploy to production');
await page.click('button:has-text("Create")');
expect(await page.textContent('text=Deploy to production')).toBeTruthy();
// Move to In Progress
const task = await page.locator('text=Deploy to production');
const inProgressZone = await page.locator('text=In Progress').locator('..').locator('.drop-zone');
await task.dragTo(inProgressZone);
expect(inProgressZone).toContainText('Deploy to production');
// Move to Done
const doneZone = await page.locator('text=Done').locator('..').locator('.drop-zone');
await task.dragTo(doneZone);
expect(doneZone).toContainText('Deploy to production');
});
test('tasks persist across page reloads', async ({ page }) => {
// Create a task
await page.click('button:has-text("New Task")');
await page.fill('input[placeholder="Task title"]', 'Persistent task');
await page.click('button:has-text("Create")');
// Reload the page
await page.reload();
// Task should still be visible
expect(await page.textContent('text=Persistent task')).toBeTruthy();
});
test('multiple users see real-time updates (WebSocket)', async ({ page, context }) => {
// Open two tabs
const page2 = await context.newPage();
await page2.goto('http://localhost:3000');
// User 1 creates a task
await page.click('button:has-text("New Task")');
await page.fill('input[placeholder="Task title"]', 'Real-time task');
await page.click('button:has-text("Create")');
// User 2 should see it immediately
await page.waitForTimeout(100); // Wait for WebSocket sync
expect(await page2.textContent('text=Real-time task')).toBeTruthy();
});
});
Playwright automates browser interactions, testing multi-user scenarios and real-time sync.
Load Testing with k6
Verify your backend scales:
npm install --save-dev k6
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Sustain
{ duration: '2m', target: 0 }, // Ramp down
],
};
export default function () {
// Test API endpoint
const res = http.post('http://localhost:8080/api/kanban/sync', {
type: 'MOVE_TASK',
payload: {
taskId: `task-${Math.random()}`,
fromColumn: 'todo',
toColumn: 'inProgress',
},
});
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 100ms': (r) => r.timings.duration < 100,
});
sleep(1);
}
Run with k6 run load-test.js. This simulates 100 concurrent users; measure response times and error rates. Aim for p95 latency under 200 ms and <1% error rate.
Setting Up CI/CD with GitHub Actions
Automate testing and deployment:
# .github/workflows/test-and-deploy.yml
name: Test and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- run: npm run lint
- run: npm run test -- --coverage
- run: npm run e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- run: npm run build
- name: Deploy to Vercel
uses: vercel/action@v4
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
This workflow:
- Runs tests and linting on every commit.
- Uploads coverage reports to Codecov.
- On main branch, builds and deploys to Vercel.
Merge to main only if all checks pass.
Deploying to Serverless Backend
Use AWS Lambda or similar for scalable APIs:
// handler.js (AWS Lambda)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, UpdateCommand } from '@aws-sdk/lib-dynamodb';
const dynamoDb = DynamoDBDocumentClient.from(new DynamoDBClient());
export const sync = async (event) => {
const { taskId, fromColumn, toColumn } = JSON.parse(event.body);
try {
// Move task in DynamoDB
await dynamoDb.send(
new UpdateCommand({
TableName: 'kanban-tasks',
Key: { id: taskId },
UpdateExpression: 'SET #col = :col',
ExpressionAttributeNames: { '#col': 'column' },
ExpressionAttributeValues: { ':col': toColumn },
})
);
// Broadcast via WebSocket (API Gateway)
// ... call WebSocket API to notify other clients
return {
statusCode: 200,
body: JSON.stringify({ success: true }),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};
Serverless functions scale automatically; you pay only for execution time. Use DynamoDB for persistence (auto-scaling) and API Gateway WebSockets for real-time updates.
Optimizing Frontend Performance
Measure and optimize Core Web Vitals:
npm install --save-dev web-vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log); // Cumulative Layout Shift
getFID(console.log); // First Input Delay
getFCP(console.log); // First Contentful Paint
getLCP(console.log); // Largest Contentful Paint
getTTFB(console.log); // Time to First Byte
Aim for:
- LCP (Largest Contentful Paint): < 2.5 seconds
- FID (First Input Delay): < 100 ms
- CLS (Cumulative Layout Shift): < 0.1
Use code splitting to reduce initial bundle:
import React from 'react';
const Board = React.lazy(() => import('./Board'));
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Board />
</React.Suspense>
);
}
Monitoring in Production
Use Sentry for error tracking and LogRocket for session replay:
npm install @sentry/react
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
});
export default Sentry.withProfiler(App);
Sentry captures errors with stack traces; LogRocket records user sessions for debugging. Monitor error rates, response times, and user behavior.
Key Takeaways
- Unit tests: Test components and business logic with React Testing Library.
- E2E tests: Validate user workflows with Playwright.
- Load testing: Verify backend scales with k6.
- CI/CD: Automate testing and deployment with GitHub Actions.
- Serverless: Use Lambda + DynamoDB for automatic scaling.
- Monitoring: Track errors and performance with Sentry and LogRocket.
Frequently Asked Questions
What test coverage should I aim for?
Aim for 70-80% code coverage. Focus on critical paths (drag-and-drop, persistence, sync) rather than 100% coverage of every function. Higher coverage with low-value tests is wasteful.
How often should I run E2E tests?
Run on every commit to main. Full E2E suites can take 5-10 minutes; use selective tests for PRs and full suite before deploy. Parallel execution in CI reduces time.
How do I test WebSocket interactions?
Mock the WebSocket in tests using jest.mock() or use a real test server. For E2E, open multiple browser contexts (as shown above) to simulate concurrent users.
What's the cost of serverless deployment at scale?
AWS Lambda charges per millisecond of execution. For a kanban app with 1000 concurrent users, expect $20-50/month on Lambda + DynamoDB. Compare with managed services like Firebase Realtime (auto-scaling, no ops overhead).
How do I minimize cold-start latency on Lambda?
Use provisioned concurrency (guarantees warm functions, adds cost) or keep functions warm with periodic pings. For sub-100 ms response times, consider always-on containers (ECS, App Engine).