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