Skip to main content

Testing Guide

Overview

Tafy Studio uses a comprehensive testing strategy across all components to ensure reliability and maintainability. This guide covers our testing approach, tools, and best practices.

Testing Philosophy

  • Test-Driven Development (TDD): Write tests before implementation when possible
  • Fast Feedback: Unit tests should run in seconds, not minutes
  • Realistic Testing: Integration tests use real services via Docker Compose
  • Continuous Testing: All tests run automatically on CI/CD
  • Coverage Goals: Aim for 80%+ coverage on critical paths

Quick Start

# Run all tests
make test

# Run unit tests only
make test-unit

# Run with coverage
make test-coverage

# Run in watch mode
make test-watch

Testing Stack

Frontend (React/TypeScript)

  • Framework: Jest
  • Testing Library: React Testing Library
  • Mocking: MSW (Mock Service Worker)
  • Coverage: Built-in Jest coverage

Backend (Python/FastAPI)

  • Framework: pytest
  • Async Testing: pytest-asyncio
  • HTTP Testing: FastAPI TestClient
  • Coverage: pytest-cov

Node Agent (Go)

  • Framework: Standard library testing
  • Assertions: testify (optional)
  • Mocking: gomock or interfaces
  • Coverage: go test -cover

Integration Testing

  • Framework: Jest with custom utilities
  • Services: Docker Compose
  • NATS Testing: Real NATS server
  • Timing: Proper wait strategies

Unit Testing

React Components

// apps/hub-ui/__tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '@/components/Button';

describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);

fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

FastAPI Endpoints

# apps/hub-api/tests/test_nodes.py
import pytest
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_list_nodes():
"""Test listing connected nodes."""
response = client.get("/api/v1/nodes")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)

@pytest.mark.asyncio
async def test_node_discovery():
"""Test async node discovery."""
# Test implementation
pass

Go Services

// apps/tafyd/discovery/discovery_test.go
package discovery

import (
"testing"
"time"
)

func TestNodeDiscovery(t *testing.T) {
d := NewDiscovery()

// Start discovery
err := d.Start()
if err != nil {
t.Fatalf("Failed to start discovery: %v", err)
}

// Wait for discovery
time.Sleep(2 * time.Second)

// Check discovered nodes
nodes := d.GetNodes()
if len(nodes) == 0 {
t.Error("Expected to discover at least one node")
}
}

Integration Test Examples

NATS Communication

// tests/integration/nats.test.js
const { connect } = require('nats');

describe('NATS Integration', () => {
let nc;

beforeAll(async () => {
nc = await connect({ servers: 'nats://localhost:4222' });
});

afterAll(async () => {
await nc.close();
});

test('publish and subscribe to HAL messages', async () => {
const subject = 'hal.v1.motor.cmd';
const message = {
hal_major: 1,
hal_minor: 0,
schema: 'tafylabs/hal/motor/differential/1.0',
device_id: 'test-device',
payload: { left: 100, right: 100 }
};

// Subscribe
const sub = nc.subscribe(subject);

// Publish
nc.publish(subject, JSON.stringify(message));

// Verify
for await (const msg of sub) {
const received = JSON.parse(msg.data);
expect(received.device_id).toBe('test-device');
break;
}
});
});

Service Communication

# tests/integration/test_hub_api_integration.py
import pytest
import httpx
from nats.aio.client import Client as NATS

@pytest.mark.integration
async def test_node_registration_flow():
"""Test complete node registration flow."""
# Connect to NATS
nc = NATS()
await nc.connect("nats://localhost:4222")

# Register node via API
async with httpx.AsyncClient() as client:
response = await client.post(
"http://localhost:8000/api/v1/nodes/register",
json={
"node_id": "test-node-123",
"capabilities": ["motor.differential:v1.0"]
}
)
assert response.status_code == 201

# Verify NATS announcement
sub = await nc.subscribe("system.node.announce")
msg = await sub.next_msg(timeout=5)
assert b"test-node-123" in msg.data

Test Organization

Directory Structure

apps/
hub-ui/
__tests__/ # Jest tests
components/ # Component tests
pages/ # Page tests
utils/ # Utility tests
jest.config.js # Jest configuration

hub-api/
tests/ # pytest tests
unit/ # Unit tests
integration/ # Integration tests
pytest.ini # pytest configuration

tafyd/
*/ # Go packages
*_test.go # Test files alongside source

tests/
integration/ # Cross-service integration tests
e2e/ # End-to-end tests
hal-compliance/ # HAL compliance tests

Naming Conventions

  • Jest: *.test.{ts,tsx,js,jsx} or *.spec.{ts,tsx,js,jsx}
  • pytest: test_*.py or *_test.py
  • Go: *_test.go

Mocking Strategies

API Mocking (MSW)

// apps/hub-ui/__tests__/mocks/handlers.ts
import { rest } from 'msw';

export const handlers = [
rest.get('/api/v1/nodes', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: 'node-1', name: 'Main Controller' },
{ id: 'node-2', name: 'Motor Driver' }
])
);
})
];

NATS Mocking

# apps/hub-api/tests/mocks/nats_mock.py
from unittest.mock import AsyncMock, MagicMock

def create_nats_mock():
nc = AsyncMock()
nc.connect = AsyncMock()
nc.publish = AsyncMock()
nc.subscribe = AsyncMock()
return nc

Coverage Requirements

Minimum Coverage Targets

  • Critical Paths: 80%+
  • Business Logic: 70%+
  • Utilities: 60%+
  • UI Components: 50%+

Running Coverage Reports

# All components
make test-coverage

# View HTML coverage reports
open apps/hub-ui/coverage/lcov-report/index.html
open apps/hub-api/htmlcov/index.html
open apps/tafyd/coverage.html

CI/CD Testing

GitHub Actions Workflow

Tests run automatically on:

  • Push to main/develop branches
  • Pull requests
  • Multiple OS versions (Ubuntu, macOS)
  • Multiple language versions

Test Matrix

strategy:
matrix:
node-version: [20, 22]
python-version: ['3.11', '3.12']
go-version: ['1.23']
os: [ubuntu-latest, macos-latest]

Performance Testing

Load Testing

# Run load tests against API
cd tests/performance
k6 run load-test.js

Benchmark Tests

// apps/tafyd/hal/benchmark_test.go
func BenchmarkMessageParsing(b *testing.B) {
message := []byte(`{"hal_major":1,"hal_minor":0,...}`)

b.ResetTimer()
for i := 0; i < b.N; i++ {
var msg HALMessage
json.Unmarshal(message, &msg)
}
}

Debugging Tests

VS Code Test Debugging

Launch configurations are provided for:

  • Jest tests (Node.js debugging)
  • pytest tests (Python debugging)
  • Go tests (Delve debugging)

Troubleshooting Common Issues

Port Conflicts

# Kill process using port
lsof -ti:4222 | xargs kill -9

Docker Services Not Ready

// Wait for services to be ready
await waitForNATS('localhost:4222', { timeout: 30000 });

Flaky Tests

  • Use proper wait strategies
  • Avoid hard-coded timeouts
  • Mock external dependencies
  • Use test retries sparingly

Best Practices

Do's

  • ✅ Write tests before fixing bugs
  • ✅ Keep tests fast and focused
  • ✅ Use descriptive test names
  • ✅ Test behavior, not implementation
  • ✅ Clean up resources in afterEach/afterAll

Don'ts

  • ❌ Don't test framework code
  • ❌ Don't use production databases
  • ❌ Don't rely on test execution order
  • ❌ Don't ignore flaky tests
  • ❌ Don't skip tests without explanation

Adding New Tests

1. Choose Test Type

  • Unit: Testing single functions/components
  • Integration: Testing service interactions
  • E2E: Testing complete user workflows

2. Write Test

Follow the patterns shown above for your language/framework

3. Run Locally

# Run your new test
make test

# Run in watch mode during development
make test-watch

4. Verify CI

Push to a branch and ensure tests pass in GitHub Actions

Resources