Testing Guide
Comprehensive testing strategies for Site Availability Monitoring.
Testing Philosophy
Our testing approach follows the testing pyramid:
/\
/ \ E2E Tests (Few)
/____\
/ \ Integration Tests (Some)
/________\
/ \ Unit Tests (Many)
/____________\
Backend Testing
Unit Tests
Test individual functions and methods:
// handlers/handlers_test.go
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
HealthHandler(w, req)
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "healthy") {
t.Error("expected 'healthy' in response body")
}
}
Integration Tests
Test component interactions:
// scraping/integration_test.go
func TestPrometheusIntegration(t *testing.T) {
// Start test Prometheus server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"up","instance":"localhost:9090","job":"prometheus"},"value":[1609459200,"1"]}]}}`))
}))
defer server.Close()
client := prometheus.NewClient(server.URL)
result, err := client.Query("up")
assert.NoError(t, err)
assert.NotNil(t, result)
}
Test Utilities
Create helper functions for common test scenarios:
// testutil/helpers.go
func SetupTestConfig() *config.Config {
return &config.Config{
ScrapeInterval: time.Second * 10,
LogLevel: "debug",
Port: 8080,
Locations: []config.Location{
{Name: "Test Location", Latitude: 40.7128, Longitude: -74.0060},
},
Apps: []config.App{
{Name: "test-app", Location: "Test Location", Metric: "up", Prometheus: "http://localhost:9090"},
},
}
}
func SetupTestServer(config *config.Config) *httptest.Server {
handler := setupRoutes(config)
return httptest.NewServer(handler)
}
Mocking
Use interfaces for easy mocking:
// Define interface
type PrometheusClient interface {
Query(query string) (*QueryResult, error)
}
// Mock implementation
type MockPrometheusClient struct {
QueryFunc func(string) (*QueryResult, error)
}
func (m *MockPrometheusClient) Query(query string) (*QueryResult, error) {
if m.QueryFunc != nil {
return m.QueryFunc(query)
}
return nil, errors.New("not implemented")
}
// Test with mock
func TestScraper(t *testing.T) {
mockClient := &MockPrometheusClient{
QueryFunc: func(query string) (*QueryResult, error) {
return &QueryResult{Value: "1"}, nil
},
}
scraper := NewScraper(mockClient)
result, err := scraper.Scrape("up")
assert.NoError(t, err)
assert.Equal(t, "1", result.Value)
}
Running Backend Tests
# Run all tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run tests with race detection
go test -race ./...
# Run specific package tests
go test ./handlers/
# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
Frontend Testing
Unit Tests
Test React components:
// AppStatusPanel.test.js
import { render, screen } from "@testing-library/react";
import { AppStatusPanel } from "./AppStatusPanel";
describe("AppStatusPanel", () => {
test("renders app status correctly", () => {
const mockApps = [
{ name: "app1", status: "up", location: "NYC" },
{ name: "app2", status: "down", location: "LA" },
];
render(<AppStatusPanel applications={mockApps} />);
expect(screen.getByText("app1")).toBeInTheDocument();
expect(screen.getByText("app2")).toBeInTheDocument();
// Check status indicators
expect(screen.getByTestId("status-up")).toBeInTheDocument();
expect(screen.getByTestId("status-down")).toBeInTheDocument();
});
test("handles loading state", () => {
render(<AppStatusPanel applications={null} loading={true} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
});
API Tests
Mock API calls:
// api.test.js
import { getApplications } from "./appStatusAPI";
// Mock fetch
global.fetch = jest.fn();
describe("API tests", () => {
beforeEach(() => {
fetch.mockClear();
});
test("getApplications returns data", async () => {
const mockData = [{ name: "app1", status: "up" }];
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});
const result = await getApplications();
expect(result).toEqual(mockData);
expect(fetch).toHaveBeenCalledWith("http://localhost:8080/api/apps");
});
test("getApplications handles errors", async () => {
fetch.mockRejectedValueOnce(new Error("Network error"));
await expect(getApplications()).rejects.toThrow("Network error");
});
});
Component Integration Tests
Test component interactions:
// Map.integration.test.js
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Map } from "./Map";
// Mock D3 and world map data
jest.mock("d3", () => ({
select: jest.fn(() => ({
append: jest.fn(() => ({ attr: jest.fn() })),
selectAll: jest.fn(() => ({ data: jest.fn() })),
})),
}));
describe("Map Integration", () => {
test("renders map with application markers", async () => {
const mockApps = [
{ name: "app1", status: "up", location: { lat: 40.7128, lon: -74.006 } },
];
render(<Map applications={mockApps} />);
await waitFor(() => {
expect(screen.getByTestId("world-map")).toBeInTheDocument();
});
// Verify marker is rendered
expect(screen.getByTestId("marker-app1")).toBeInTheDocument();
});
test("handles marker click events", async () => {
const mockOnMarkerClick = jest.fn();
const mockApps = [
{ name: "app1", status: "up", location: { lat: 40.7128, lon: -74.006 } },
];
render(<Map applications={mockApps} onMarkerClick={mockOnMarkerClick} />);
const marker = screen.getByTestId("marker-app1");
await userEvent.click(marker);
expect(mockOnMarkerClick).toHaveBeenCalledWith("app1");
});
});
Running Frontend Tests
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run tests with coverage
npm test -- --coverage
# Run specific test file
npm test -- AppStatusPanel.test.js
# Update snapshots
npm test -- --updateSnapshot
End-to-End Testing
Cypress Setup
# Install Cypress
npm install --save-dev cypress
# Open Cypress
npx cypress open
E2E Test Examples
// cypress/e2e/app.cy.js
describe("Site Availability Monitoring", () => {
beforeEach(() => {
// Start backend server in test mode
cy.exec("npm run start:test");
cy.visit("http://localhost:3000");
});
it("displays the world map", () => {
cy.get('[data-testid="world-map"]').should("be.visible");
});
it("shows application statuses", () => {
cy.get('[data-testid="sidebar"]').should("contain", "Applications");
cy.get('[data-testid="app-list"]').should("exist");
});
it("updates data in real-time", () => {
// Wait for initial load
cy.get('[data-testid="app-status"]').should("contain", "up");
// Mock backend to return different status
cy.intercept("GET", "/api/apps", { fixture: "apps-down.json" });
// Wait for update
cy.get('[data-testid="app-status"]', { timeout: 10000 }).should(
"contain",
"down",
);
});
it("handles API errors gracefully", () => {
cy.intercept("GET", "/api/apps", { statusCode: 500 });
cy.get('[data-testid="error-message"]').should(
"contain",
"Failed to load applications",
);
});
});
Visual Regression Testing
// cypress/e2e/visual.cy.js
describe("Visual Regression Tests", () => {
it("matches baseline screenshot", () => {
cy.visit("http://localhost:3000");
cy.get('[data-testid="world-map"]').should("be.visible");
// Take screenshot and compare
cy.matchImageSnapshot("world-map-baseline");
});
});
Performance Testing
Load Testing with Artillery
# artillery.yml
config:
target: "http://localhost:8080"
phases:
- duration: 60
arrivalRate: 10
processor: "./test-functions.js"
scenarios:
- name: "API Load Test"
flow:
- get:
url: "/api/apps"
- get:
url: "/api/locations"
- get:
url: "/health"
Benchmark Tests
// benchmark_test.go
func BenchmarkHealthHandler(b *testing.B) {
req := httptest.NewRequest("GET", "/health", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
HealthHandler(w, req)
}
}
func BenchmarkPrometheusQuery(b *testing.B) {
client := prometheus.NewClient("http://localhost:9090")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.Query("up")
if err != nil {
b.Fatal(err)
}
}
}
Test Data Management
Fixtures
Create reusable test data:
// cypress/fixtures/apps.json
[
{
name: "frontend",
status: "up",
location: "New York",
last_check: "2023-12-01T10:00:00Z",
},
{
name: "backend",
status: "down",
location: "London",
last_check: "2023-12-01T10:00:00Z",
},
];
Test Database
For integration tests requiring data:
func setupTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
// Run migrations
if err := runMigrations(db); err != nil {
t.Fatal(err)
}
// Seed test data
if err := seedTestData(db); err != nil {
t.Fatal(err)
}
return db
}
Continuous Integration
GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
backend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Run backend tests
run: |
cd backend
go test -race -cover ./...
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: |
cd frontend
npm ci
- name: Run frontend tests
run: |
cd frontend
npm test -- --coverage --watchAll=false
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run E2E tests
run: |
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
Best Practices
Test Organization
- Group related tests in describe blocks
- Use clear, descriptive test names
- Follow AAA pattern (Arrange, Act, Assert)
- Keep tests independent and isolated
Test Coverage
- Aim for 80%+ code coverage
- Focus on critical paths
- Don't sacrifice quality for coverage
- Use coverage reports to find gaps
Test Maintenance
- Keep tests simple and focused
- Update tests when code changes
- Remove obsolete tests
- Refactor test code like production code
Common Pitfalls
- Flaky tests: Use proper waits and timeouts
- Slow tests: Mock external dependencies
- Brittle tests: Avoid testing implementation details
- Incomplete tests: Test error scenarios too
Remember: Good tests are your safety net for confident refactoring and feature development!