Skip to main content

Testing Guide

Testing strategies and commands for Almafrica.

Testing Philosophy

  • Test behavior, not implementation - Focus on what, not how
  • Test at appropriate level - Unit for logic, integration for APIs, E2E for flows
  • Keep tests fast - Slow tests get skipped
  • Maintain test independence - Tests shouldn't depend on each other

Backend Testing (.NET)

Structure

backend/
├── Almafrica.Tests/
│ ├── Unit/
│ │ ├── Services/
│ │ └── Validators/
│ ├── Integration/
│ │ ├── Controllers/
│ │ └── Repositories/
│ └── TestData/

Commands

cd backend

# Run all tests
dotnet test

# Run specific test class
dotnet test --filter "FullyQualifiedName~FarmerServiceTests"

# Run specific test method
dotnet test --filter "FullyQualifiedName~FarmerServiceTests.CreateFarmer_ValidData_ReturnsSuccess"

# Run with coverage
dotnet test --collect:"XPlat Code Coverage"

# Run only unit tests
dotnet test --filter "FullyQualifiedName~Unit"

# Run only integration tests
dotnet test --filter "FullyQualifiedName~Integration"

Unit Test Example

public class FarmerServiceTests
{
private readonly Mock<IFarmerRepository> _repositoryMock;
private readonly FarmerService _sut;

public FarmerServiceTests()
{
_repositoryMock = new Mock<IFarmerRepository>();
_sut = new FarmerService(_repositoryMock.Object);
}

[Fact]
public async Task CreateFarmer_ValidData_ReturnsSuccess()
{
// Arrange
var dto = new CreateFarmerDto { Name = "John Doe" };
_repositoryMock.Setup(r => r.AddAsync(It.IsAny<Farmer>()))
.ReturnsAsync(new Farmer { Id = Guid.NewGuid(), Name = "John Doe" });

// Act
var result = await _sut.CreateFarmerAsync(dto);

// Assert
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
}

Integration Test Example

public class FarmersControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;

public FarmersControllerTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient();
}

[Fact]
public async Task GetFarmers_ReturnsSuccessAndList()
{
// Act
var response = await _client.GetAsync("/api/farmers");

// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("farmers", content);
}
}

Web Testing (Next.js)

Structure

web/almafrica-web/
├── __tests__/
│ ├── unit/
│ │ ├── components/
│ │ └── lib/
│ └── integration/
├── e2e/
│ └── specs/
└── jest.config.ts

Commands

cd web/almafrica-web

# Run unit tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run with coverage
pnpm test:coverage

# Run e2e tests
pnpm test:e2e

# Run specific test file
pnpm test farmer.test.ts

Unit Test Example

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FarmerCard } from './FarmerCard'

describe('FarmerCard', () => {
it('displays farmer name and farm count', () => {
const farmer = {
id: '1',
name: 'John Doe',
farmCount: 3,
}

render(<FarmerCard farmer={farmer} />)

expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('3 farms')).toBeInTheDocument()
})

it('calls onClick when clicked', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
const farmer = { id: '1', name: 'John Doe' }

render(<FarmerCard farmer={farmer} onClick={onClick} />)

await user.click(screen.getByRole('button'))

expect(onClick).toHaveBeenCalledWith('1')
})
})

E2E Test Example

import { test, expect } from '@playwright/test'

test('farmer list displays farmers', async ({ page }) => {
await page.goto('/farmers')

await expect(page.locator('h1')).toContainText('Farmers')
await expect(page.locator('[data-testid="farmer-card"]')).toHaveCountGreaterThan(0)
})

test('can create a new farmer', async ({ page }) => {
await page.goto('/farmers/new')

await page.fill('[name="name"]', 'Test Farmer')
await page.fill('[name="phone"]', '+1234567890')
await page.click('button[type="submit"]')

await expect(page).toHaveURL('/farmers')
await expect(page.locator('.toast')).toContainText('Farmer created')
})

Mobile Testing (Flutter)

Structure

mobile/mon_jardin/
├── test/
│ ├── unit/
│ │ ├── services/
│ │ └── repositories/
│ └── widget/
│ └── screens/
└── integration_test/
└── flows/

Commands

cd mobile/mon_jardin

# Run unit tests
flutter test

# Run specific test file
flutter test test/unit/services/sync_service_test.dart

# Run with coverage
flutter test --coverage

# Run widget tests
flutter test test/widget/

# Run integration tests
flutter test integration_test/

# Generate coverage report
genhtml coverage/lcov.info -o coverage/html

Unit Test Example

void main() {
group('SyncService', () {
late SyncService syncService;
late MockSyncRepository mockRepository;

setUp(() {
mockRepository = MockSyncRepository();
syncService = SyncService(mockRepository);
});

test('syncAll returns success when all operations succeed', () async {
// Arrange
when(mockRepository.getPendingCount())
.thenAnswer((_) async => 5);
when(mockRepository.syncPending())
.thenAnswer((_) async => true);

// Act
final result = await syncService.syncAll();

// Assert
expect(result, true);
verify(mockRepository.syncPending()).called(1);
});
});
}

Widget Test Example

void main() {
group('FarmerListScreen', () {
testWidgets('displays farmers when loaded', (tester) async {
// Arrange
final farmers = [
Farmer(id: '1', name: 'John Doe'),
Farmer(id: '2', name: 'Jane Smith'),
];

// Act
await tester.pumpWidget(
MaterialApp(
home: FarmerListScreen(farmers: farmers),
),
);

// Assert
expect(find.text('John Doe'), findsOneWidget);
expect(find.text('Jane Smith'), findsOneWidget);
});
});
}

Test Data

Backend

Use Bogus for generating test data:

var faker = new Faker<CreateFarmerDto>()
.RuleFor(f => f.Name, f => f.Name.FullName())
.RuleFor(f => f.Phone, f => f.Phone.PhoneNumber());

var testFarmer = faker.Generate();

Web

Use MSW for API mocking:

import { rest } from 'msw'

export const handlers = [
rest.get('/api/farmers', (req, res, ctx) => {
return res(ctx.json({ farmers: mockFarmers }))
}),
]

Mobile

Use mocktail for mocking:

class MockFarmerRepository extends Mock implements FarmerRepository {}

void main() {
late MockFarmerRepository mockRepository;

setUp(() {
mockRepository = MockFarmerRepository();
registerFallbackValue(Farmer(id: '', name: ''));
});
}

Coverage Goals

LayerTargetCurrent
Backend80%--
Web70%--
Mobile70%--

Best Practices

  1. Write tests first for new features (TDD)
  2. One assertion per test when possible
  3. Use descriptive test names that explain the scenario
  4. Don't test implementation details - test behavior
  5. Keep tests isolated - no shared state
  6. Mock external dependencies - APIs, databases, file system

CI Integration

Tests run automatically in CI:

# Example GitHub Actions
- name: Run Backend Tests
run: dotnet test --configuration Release

- name: Run Web Tests
run: pnpm test:ci

- name: Run Mobile Tests
run: flutter test --coverage