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
| Layer | Target | Current |
|---|---|---|
| Backend | 80% | -- |
| Web | 70% | -- |
| Mobile | 70% | -- |
Best Practices
- Write tests first for new features (TDD)
- One assertion per test when possible
- Use descriptive test names that explain the scenario
- Don't test implementation details - test behavior
- Keep tests isolated - no shared state
- 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