Testing Guide
This comprehensive guide covers all aspects of testing in the Logic Apps UX project, from unit tests to end-to-end testing.
Testing Philosophy
We follow a testing pyramid approach:
- Unit Tests: Fast, focused, numerous
- Integration Tests: Test component interactions
- E2E Tests: Test complete user flows
Unit Testing
Framework and Tools
We use Vitest with React Testing Library for unit testing:
- Vitest: Fast, Vite-native test runner
- React Testing Library: Testing from user's perspective
- MSW: Mock Service Worker for API mocking
- Testing Library User Event: Realistic user interactions
Writing Unit Tests
Basic Component Test
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { WorkflowCard } from './WorkflowCard';
describe('WorkflowCard', () => {
it('should display workflow name', () => {
render(<WorkflowCard name="My Workflow" status="Running" />);
expect(screen.getByText('My Workflow')).toBeInTheDocument();
expect(screen.getByText('Running')).toBeInTheDocument();
});
it('should handle click events', async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<WorkflowCard name="My Workflow" onClick={handleClick} />);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
Testing Hooks
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should increment counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Testing Redux State
import { renderWithProviders } from '@/test-utils';
import { screen } from '@testing-library/react';
import { DesignerPanel } from './DesignerPanel';
describe('DesignerPanel with Redux', () => {
it('should display workflow from store', () => {
const preloadedState = {
designer: {
workflow: {
name: 'Test Workflow',
id: '123'
}
}
};
renderWithProviders(<DesignerPanel />, { preloadedState });
expect(screen.getByText('Test Workflow')).toBeInTheDocument();
});
});
Running Unit Tests
# Run all unit tests
pnpm run test:lib
# Run tests in watch mode
pnpm vitest --watch
# Run tests with coverage
pnpm vitest --coverage
# Run specific test file
pnpm vitest Button.test.tsx
# Run tests matching pattern
pnpm vitest --grep "should handle click"
Test Coverage
We aim for:
- 80%+ coverage for utilities and hooks
- 70%+ coverage for components
- Critical paths must have 100% coverage
View coverage report:
pnpm vitest --coverage --ui
Integration Testing
Testing Component Integration
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { WorkflowList } from './WorkflowList';
// Setup MSW server
const server = setupServer(
rest.get('/api/workflows', (req, res, ctx) => {
return res(ctx.json([
{ id: '1', name: 'Workflow 1' },
{ id: '2', name: 'Workflow 2' }
]));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('WorkflowList Integration', () => {
it('should load and display workflows', async () => {
render(<WorkflowList />);
// Wait for loading to finish
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// Check workflows are displayed
expect(screen.getByText('Workflow 1')).toBeInTheDocument();
expect(screen.getByText('Workflow 2')).toBeInTheDocument();
});
});
End-to-End Testing
Playwright Setup
We use Playwright for E2E testing across browsers.
Test Categories
-
Mock API Tests (
@mock
tag)- Run with mock data
- No external dependencies
- Fast and reliable
- Run on every PR
-
Real API Tests
- Test against live Azure services
- Require authentication
- Run hourly in CI
- More comprehensive
Writing E2E Tests
Basic E2E Test
import { test, expect } from '@playwright/test';
test.describe('Workflow Designer E2E', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should create a simple workflow @mock', async ({ page }) => {
// Click create workflow button
await page.getByRole('button', { name: 'Create workflow' }).click();
// Add HTTP trigger
await page.getByTestId('add-trigger').click();
await page.getByText('When a HTTP request is received').click();
// Configure trigger
await page.getByLabel('Method').selectOption('POST');
// Add action
await page.getByTestId('add-action').click();
await page.getByPlaceholder('Search actions').fill('response');
await page.getByText('Response').first().click();
// Save workflow
await page.getByRole('button', { name: 'Save' }).click();
// Verify success
await expect(page.getByText('Workflow saved')).toBeVisible();
});
});
Page Object Model
// pages/DesignerPage.ts
import { Page } from '@playwright/test';
export class DesignerPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/designer');
}
async addTrigger(triggerName: string) {
await this.page.getByTestId('add-trigger').click();
await this.page.getByText(triggerName).click();
}
async addAction(actionName: string) {
await this.page.getByTestId('add-action').click();
await this.page.getByPlaceholder('Search actions').fill(actionName);
await this.page.getByText(actionName).first().click();
}
async saveWorkflow() {
await this.page.getByRole('button', { name: 'Save' }).click();
}
}
// tests/workflow.spec.ts
import { test, expect } from '@playwright/test';
import { DesignerPage } from '../pages/DesignerPage';
test('create workflow using page object', async ({ page }) => {
const designer = new DesignerPage(page);
await designer.goto();
await designer.addTrigger('Manual trigger');
await designer.addAction('Send an email');
await designer.saveWorkflow();
await expect(page.getByText('Workflow saved')).toBeVisible();
});
Running E2E Tests
Mock API Tests
# Run mock API tests
pnpm run test:e2e --grep @mock
# Run specific test file
pnpm run test:e2e designer/workflow.spec.ts
# Run in headed mode (see browser)
pnpm run test:e2e --headed
# Run in debug mode
pnpm run test:e2e --debug
Real API Tests
First, set up environment variables:
# Create .env file in root
AZURE_SITE_NAME="your-logic-app-name"
AZURE_SUBSCRIPTION_ID="your-subscription-id"
AZURE_RESOURCE_GROUP="your-resource-group"
AZURE_MANAGEMENT_TOKEN="your-arm-token"
Generate ARM token:
pnpm run generateArmToken
Run tests:
# Run all real API tests
pnpm run test:e2e
# Run specific real API test
pnpm run test:e2e --grep "real api test name"
E2E Test Best Practices
- Use data-testid for reliable element selection
- Wait for elements instead of arbitrary delays
- Clean up resources after tests
- Use fixtures for test data
- Parallelize tests when possible
// Good practices
test.describe.parallel('Workflow Tests', () => {
test('test 1', async ({ page }) => {
// Use data-testid
await page.getByTestId('workflow-trigger').click();
// Wait for elements
await expect(page.getByText('Saved')).toBeVisible();
// Clean up
await page.getByTestId('delete-workflow').click();
});
});
Debugging E2E Tests
Using Playwright Inspector
# Run with inspector
pnpm run test:e2e --debug
# Use page.pause() in tests
test('debug this test', async ({ page }) => {
await page.goto('/');
await page.pause(); // Opens inspector here
await page.click('button');
});
View Test Reports
# After test run
pnpm exec playwright show-report
# View trace files
pnpm exec playwright show-trace trace.zip
Common Issues
Port 4200 still in use
# macOS/Linux
lsof -ti:4200 | xargs kill -9
# Windows
netstat -ano | findstr :4200
taskkill /PID <pid> /F
Tests timing out
// Increase timeout for specific test
test('slow test', async ({ page }) => {
test.setTimeout(60000); // 60 seconds
// test code
});
Test Organization
File Structure
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # Unit tests
│ │ └── Button.stories.tsx # Storybook stories
├── hooks/
│ ├── useWorkflow.ts
│ └── useWorkflow.test.ts
├── utils/
│ ├── validation.ts
│ └── validation.test.ts
e2e/
├── designer/
│ ├── workflow.spec.ts
│ └── fixtures/
│ └── workflows.json
├── data-mapper/
│ └── mapping.spec.ts
└── pages/
└── DesignerPage.ts
Test Utilities
Create reusable test utilities:
// test-utils/render.tsx
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { IntlProvider } from 'react-intl';
export function renderWithProviders(
ui: React.ReactElement,
options?: RenderOptions
) {
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<Provider store={store}>
<IntlProvider locale="en">
{children}
</IntlProvider>
</Provider>
);
}
return render(ui, { wrapper: Wrapper, ...options });
}
Continuous Integration
GitHub Actions
Tests run automatically on:
- Pull request creation
- Commits to main
- Scheduled (hourly for real API tests)
Test Requirements
PRs must pass:
- ✅ All unit tests
- ✅ All mock E2E tests
- ✅ Linting and type checking
- ✅ Minimum code coverage
Performance Testing
Component Performance
import { render } from '@testing-library/react';
import { measureRender } from '@/test-utils/performance';
test('WorkflowCanvas renders efficiently', async () => {
const renderTime = await measureRender(
<WorkflowCanvas nodes={largeNodeSet} />
);
expect(renderTime).toBeLessThan(100); // ms
});
Bundle Size Testing
# Check bundle size
pnpm run build:lib -- --metafile
pnpm exec esbuild-visualizer
# Set size limits in package.json
"size-limit": [
{
"path": "dist/index.js",
"limit": "500 KB"
}
]
Accessibility Testing
Automated A11y Tests
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('Button is accessible', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Testing Checklist
Before submitting PR:
- Unit tests written for new code
- Integration tests for complex features
- E2E tests for user flows
- Tests pass locally
- Coverage meets requirements
- No skipped or commented tests
- Test names are descriptive
- Mocks are properly cleaned up
Happy testing! 🧪