- Test-Driven Development (TDD): Quando possível, escrever testes antes do código
- Testes Automatizados: Todos os testes devem ser executáveis automaticamente
- Testes Isolados: Cada teste deve ser independente e isolado
- Testes Rápidos: Unit tests devem ser rápidos (< 100ms cada)
- Cobertura Adequada: Focar em código crítico (lógica de negócio, segurança)
- Testes Mantíveis: Testes devem ser fáceis de ler, entender e manter
FLUTTER TESTING STRATEGY
alize os proximos passos em toda a documentação para # Estratégia de Testes - Arah Flutter App
Versão: 1.0
Data: 2025-01-20
Status: 📋 Estratégia Completa
Tipo: Documentação Técnica de Testes
🎯 Visão Geral
Objetivo
Este documento especifica a estratégia completa de testes para o app Flutter Arah, cobrindo todos os níveis de teste necessários para garantir qualidade, confiabilidade e manutenibilidade do código.
┌─────────────┐
│ E2E Tests │ (10%) - Integration Tests
│ │ - Testes de fluxo completo
├─────────────┤
│ Widget Tests│ (30%) - UI Tests
│ │ - Componentes e telas
├─────────────┤
│ Unit Tests │ (60%) - Lógica de negócio
│ │ - Services, Repositories
└─────────────┘
📊 Tipos de Testes
Objetivo: Testar lógica de negócio isolada (funções, métodos, classes)
Onde usar:
- Services (API, Storage, Navigation)
- Repositories
- Models (validações, serialização)
- Utilitários (helpers, validators)
- Providers (lógica de estado)
Ferramentas:
flutter_test(padrão)mockito(mocks)fake_async(timers assíncronos)
Objetivo: Testar widgets isolados e interações de UI
Onde usar:
- Componentes reutilizáveis (buttons, cards, inputs)
- Telas completas (screens)
- Navegação (rotas, deep linking)
- Estados visuais (loading, error, success)
Ferramentas:
flutter_test(WidgetTester)mocktail(mocks para Riverpod)golden_toolkit(golden tests)
Objetivo: Testar fluxos completos end-to-end
Onde usar:
- Jornadas críticas do usuário (onboarding, autenticação, criação de post)
- Fluxos multi-tela
- Integração com serviços externos (Firebase, API)
Ferramentas:
integration_test(Flutter SDK)flutter_driver(legacy, não recomendado)
4. Golden Tests
Objetivo: Testar renderização visual de componentes
Onde usar:
- Design system components
- Telas críticas (verificação visual)
- Dark mode (comparação light/dark)
Ferramentas:
golden_toolkitflutter_test(matchesGoldenFile)
test/
├── unit/ # Unit Tests
│ ├── core/
│ │ ├── network/
│ │ │ └── dio_client_test.dart
│ │ ├── storage/
│ │ │ └── secure_storage_test.dart
│ │ └── utils/
│ │ └── validators_test.dart
│ ├── shared/
│ │ ├── services/
│ │ │ ├── observability/
│ │ │ │ ├── metrics_service_test.dart
│ │ │ │ ├── logging_service_test.dart
│ │ │ │ └── exception_service_test.dart
│ │ │ └── privacy/
│ │ │ └── consent_service_test.dart
│ │ └── providers/
│ │ ├── auth_provider_test.dart
│ │ └── session_provider_test.dart
│ └── features/
│ ├── auth/
│ │ ├── data/
│ │ │ └── auth_repository_test.dart
│ │ └── domain/
│ │ └── auth_service_test.dart
│ ├── feed/
│ │ └── data/
│ │ └── feed_repository_test.dart
│ └── territories/
│ └── data/
│ └── territory_repository_test.dart
│
├── widget/ # Widget Tests
│ ├── shared/
│ │ └── widgets/
│ │ ├── buttons/
│ │ │ └── primary_button_test.dart
│ │ ├── cards/
│ │ │ ├── post_card_test.dart
│ │ │ └── glass_card_test.dart
│ │ └── inputs/
│ │ └── text_field_test.dart
│ └── features/
│ ├── feed/
│ │ ├── screens/
│ │ │ ├── feed_screen_test.dart
│ │ │ └── post_detail_screen_test.dart
│ │ └── widgets/
│ │ └── post_list_test.dart
│ ├── auth/
│ │ └── screens/
│ │ └── login_screen_test.dart
│ └── territories/
│ └── screens/
│ └── territory_list_screen_test.dart
│
├── integration/ # Integration Tests
│ ├── flows/
│ │ ├── onboarding_flow_test.dart
│ │ ├── authentication_flow_test.dart
│ │ ├── create_post_flow_test.dart
│ │ └── territory_selection_flow_test.dart
│ └── helpers/
│ └── test_helpers.dart
│
├── golden/ # Golden Tests
│ ├── components/
│ │ ├── primary_button_golden_test.dart
│ │ └── post_card_golden_test.dart
│ └── screens/
│ ├── feed_screen_golden_test.dart
│ └── profile_screen_golden_test.dart
│
└── helpers/ # Test Helpers
├── mock_factories.dart
├── test_data.dart
├── test_widgets.dart
└── test_providers.dart
🧩 Widget Tests
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mocktail/mocktail.dart';
void main() {
group('PrimaryButton Widget Tests', () {
testWidgets('should display button with text', (WidgetTester tester) async {
// Arrange
const buttonText = 'Click Me';
var tapped = false;
// Act
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PrimaryButton(
text: buttonText,
onPressed: () => tapped = true,
),
),
),
);
// Assert
expect(find.text(buttonText), findsOneWidget);
expect(tapped, false);
// Act - Tap button
await tester.tap(find.text(buttonText));
await tester.pump();
// Assert
expect(tapped, true);
});
testWidgets('should be disabled when onPressed is null', (WidgetTester tester) async {
// Arrange
const buttonText = 'Disabled Button';
// Act
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PrimaryButton(
text: buttonText,
onPressed: null,
),
),
),
);
// Assert
final button = tester.widget<PrimaryButton>(find.text(buttonText));
expect(button.onPressed, isNull);
// Try to tap (should not crash)
await tester.tap(find.text(buttonText));
await tester.pump();
});
});
}
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mocktail/mocktail.dart';
// Mock Provider
class MockFeedRepository extends Mock implements FeedRepository {}
void main() {
group('FeedScreen Widget Tests', () {
late MockFeedRepository mockRepository;
setUp(() {
mockRepository = MockFeedRepository();
});
testWidgets('should display loading indicator when loading', (WidgetTester tester) async {
// Arrange
when(() => mockRepository.getFeed(territoryId: any(named: 'territoryId')))
.thenAnswer((_) async => []);
// Act
await tester.pumpWidget(
ProviderScope(
overrides: [
feedRepositoryProvider.overrideWithValue(mockRepository),
],
child: MaterialApp(
home: FeedScreen(territoryId: 'test-territory-id'),
),
),
);
// Assert
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('should display posts when loaded', (WidgetTester tester) async {
// Arrange
final posts = [
Post(id: '1', title: 'Post 1', content: 'Content 1'),
Post(id: '2', title: 'Post 2', content: 'Content 2'),
];
when(() => mockRepository.getFeed(territoryId: any(named: 'territoryId')))
.thenAnswer((_) async => posts);
// Act
await tester.pumpWidget(
ProviderScope(
overrides: [
feedRepositoryProvider.overrideWithValue(mockRepository),
],
child: MaterialApp(
home: FeedScreen(territoryId: 'test-territory-id'),
),
),
);
// Wait for async loading
await tester.pumpAndSettle();
// Assert
expect(find.text('Post 1'), findsOneWidget);
expect(find.text('Post 2'), findsOneWidget);
expect(find.byType(PostCard), findsNWidgets(2));
});
testWidgets('should display error message when error occurs', (WidgetTester tester) async {
// Arrange
when(() => mockRepository.getFeed(territoryId: any(named: 'territoryId')))
.thenThrow(Exception('Network error'));
// Act
await tester.pumpWidget(
ProviderScope(
overrides: [
feedRepositoryProvider.overrideWithValue(mockRepository),
],
child: MaterialApp(
home: FeedScreen(territoryId: 'test-territory-id'),
),
),
);
// Wait for async loading
await tester.pumpAndSettle();
// Assert
expect(find.text('Erro ao carregar feed'), findsOneWidget);
expect(find.text('Network error'), findsOneWidget);
});
});
}
testWidgets('should handle pull-to-refresh', (WidgetTester tester) async {
// Arrange
final posts = [Post(id: '1', title: 'Post 1')];
when(() => mockRepository.getFeed(territoryId: any(named: 'territoryId')))
.thenAnswer((_) async => posts);
await tester.pumpWidget(
ProviderScope(
overrides: [feedRepositoryProvider.overrideWithValue(mockRepository)],
child: MaterialApp(home: FeedScreen(territoryId: 'test-id')),
),
);
await tester.pumpAndSettle();
// Act - Pull to refresh
final gesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await gesture.moveBy(Offset(0, 300));
await tester.pumpAndSettle();
// Assert
verify(() => mockRepository.getFeed(territoryId: 'test-id')).called(2); // Initial + refresh
});
🔄 Integration Tests
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:Arah/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Onboarding Flow Integration Test', () {
testWidgets('complete onboarding flow', (WidgetTester tester) async {
// Start app
app.main();
await tester.pumpAndSettle();
// Step 1: Splash screen should appear
expect(find.text('Arah'), findsOneWidget);
await tester.pumpAndSettle(Duration(seconds: 2));
// Step 2: Onboarding screen should appear
expect(find.text('Bem-vinda ao Arah'), findsOneWidget);
await tester.tap(find.text('Começar'));
await tester.pumpAndSettle();
// Step 3: Location permission screen
expect(find.text('Descubra seu território'), findsOneWidget);
await tester.tap(find.text('Permitir Localização'));
await tester.pumpAndSettle();
// Step 4: Login screen should appear
expect(find.text('Entre ou crie sua conta'), findsOneWidget);
});
});
group('Authentication Flow Integration Test', () {
testWidgets('complete authentication flow', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// Skip onboarding if present
if (find.text('Começar').evaluate().isNotEmpty) {
await tester.tap(find.text('Começar'));
await tester.pumpAndSettle();
}
// Tap Google Sign-In
await tester.tap(find.text('Continuar com Google'));
await tester.pumpAndSettle();
// Wait for authentication (mock or real)
await tester.pumpAndSettle(Duration(seconds: 3));
// Should navigate to territory selection
expect(find.text('Encontre seu território'), findsOneWidget);
});
});
group('Create Post Flow Integration Test', () {
testWidgets('create and publish a post', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// Navigate to feed
await tester.tap(find.byIcon(Icons.home));
await tester.pumpAndSettle();
// Tap FAB to create post
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
// Fill form
await tester.enterText(find.byType(TextField).first, 'Título do Post');
await tester.enterText(find.byType(TextField).last, 'Conteúdo do post aqui...');
await tester.pumpAndSettle();
// Select visibility
await tester.tap(find.text('Público'));
await tester.pumpAndSettle();
// Tap publish
await tester.tap(find.text('Publicar'));
await tester.pumpAndSettle();
// Should navigate back to feed with success message
expect(find.text('Post publicado com sucesso'), findsOneWidget);
expect(find.text('Título do Post'), findsOneWidget);
});
});
}
// test/helpers/test_helpers.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class TestHelpers {
static Future<void> waitForAppToLoad(WidgetTester tester) async {
await tester.pumpAndSettle(Duration(seconds: 2));
}
static Future<void> tapButtonWithText(WidgetTester tester, String text) async {
await tester.tap(find.text(text));
await tester.pumpAndSettle();
}
static Future<void> enterTextInField(WidgetTester tester, String text, int fieldIndex) async {
final fields = find.byType(TextField);
await tester.enterText(fields.at(fieldIndex), text);
await tester.pumpAndSettle();
}
static Future<void> scrollToWidget(WidgetTester tester, Finder finder) async {
await tester.scrollUntilVisible(finder, 500, scrollable: find.byType(Scrollable));
await tester.pumpAndSettle();
}
}
🎨 Golden Tests
Setup
# pubspec.yaml
dev_dependencies:
golden_toolkit: ^0.15.0
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:Arah/shared/widgets/buttons/primary_button.dart';
void main() {
group('PrimaryButton Golden Tests', () {
testGoldens('should match primary button golden', (WidgetTester tester) async {
// Arrange
final builder = DeviceBuilder()
..overrideDevicesForAllScenarios(devices: [
Device.phone,
Device.tabletPortrait,
Device.tabletLandscape,
])
..addScenario(
name: 'default',
widget: PrimaryButton(
text: 'Clique aqui',
onPressed: () {},
),
)
..addScenario(
name: 'disabled',
widget: PrimaryButton(
text: 'Desabilitado',
onPressed: null,
),
);
// Act & Assert
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'primary_button');
});
testGoldens('should match primary button in dark mode', (WidgetTester tester) async {
// Arrange
final widget = MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.dark,
home: Scaffold(
body: PrimaryButton(
text: 'Clique aqui',
onPressed: () {},
),
),
);
// Act & Assert
await tester.pumpWidget(widget);
await screenMatchesGolden(tester, 'primary_button_dark');
});
});
}
Executar Golden Tests
flutter test --update-goldens
flutter test integration_test/golden/
♿ Testes de Acessibilidade
testWidgets('should have correct semantics for accessibility', (WidgetTester tester) async {
// Arrange
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PrimaryButton(
text: 'Clique aqui',
onPressed: () {},
),
),
),
);
// Assert
expect(
find.bySemanticsLabel('Clique aqui'),
findsOneWidget,
reason: 'Button should have semantic label',
);
final semantics = tester.getSemantics(find.byType(PrimaryButton));
expect(semantics.hasTapAction, true, reason: 'Button should be tappable');
expect(semantics.isButton, true, reason: 'Button should be identified as button');
});
testWidgets('should be accessible', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: FeedScreen(territoryId: 'test-id'),
),
);
await tester.pumpAndSettle();
// Verify semantics tree
final semantics = tester.getSemantics(find.byType(FeedScreen));
// Check for proper labels
expect(semantics.hasLabel, true);
// Check for proper hint
expect(semantics.hasHint, true);
// Check for proper actions
expect(semantics.hasTapAction || semantics.hasScrollAction, true);
});
⚡ Testes de Performance
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('feed screen should render within performance budget', (WidgetTester tester) async {
// Arrange
final posts = List.generate(50, (i) => Post(id: '$i', title: 'Post $i'));
// Act
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: FeedScreen(posts: posts),
),
),
);
await tester.pumpAndSettle();
stopwatch.stop();
// Assert
expect(stopwatch.elapsedMilliseconds, lessThan(500),
reason: 'Feed screen should render in less than 500ms',
);
});
testWidgets('post card should render efficiently', (WidgetTester tester) async {
// Arrange
final post = Post(id: '1', title: 'Test Post', content: 'Content');
// Act
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PostCard(post: post),
),
),
);
await tester.pumpAndSettle();
stopwatch.stop();
// Assert
expect(stopwatch.elapsedMilliseconds, lessThan(100),
reason: 'Post card should render in less than 100ms',
);
});
}
🌐 Testes de API (Mock)
import 'package:mocktail/mocktail.dart';
class MockFeedRepository extends Mock implements FeedRepository {}
void main() {
group('FeedRepository Unit Tests', () {
late MockFeedRepository repository;
setUp(() {
repository = MockFeedRepository();
});
test('should return posts when API call succeeds', () async {
// Arrange
final expectedPosts = [
Post(id: '1', title: 'Post 1'),
Post(id: '2', title: 'Post 2'),
];
when(() => repository.getFeed(territoryId: any(named: 'territoryId')))
.thenAnswer((_) async => expectedPosts);
// Act
final result = await repository.getFeed(territoryId: 'test-id');
// Assert
expect(result, equals(expectedPosts));
verify(() => repository.getFeed(territoryId: 'test-id')).called(1);
});
test('should throw exception when API call fails', () async {
// Arrange
when(() => repository.getFeed(territoryId: any(named: 'territoryId')))
.thenThrow(Exception('Network error'));
// Act & Assert
expect(
() => repository.getFeed(territoryId: 'test-id'),
throwsException,
);
});
});
}
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
class MockDio extends Mock implements Dio {}
void main() {
group('ApiService Unit Tests', () {
late MockDio mockDio;
late ApiService apiService;
setUp(() {
mockDio = MockDio();
apiService = ApiService(dio: mockDio);
});
test('should return data when GET request succeeds', () async {
// Arrange
final responseData = {'id': '1', 'title': 'Post 1'};
when(() => mockDio.get(any()))
.thenAnswer((_) async => Response(
data: responseData,
statusCode: 200,
requestOptions: RequestOptions(path: '/feed'),
));
// Act
final result = await apiService.getFeed(territoryId: 'test-id');
// Assert
expect(result, isNotNull);
verify(() => mockDio.get('/feed')).called(1);
});
});
}
🔄 CI/CD e Testes Automatizados
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.0'
channel: 'stable'
- name: Get dependencies
run: flutter pub get
- name: Run unit tests
run: flutter test test/unit/
- name: Run widget tests
run: flutter test test/widget/
- name: Run integration tests (if configured)
run: flutter test integration_test/
continue-on-error: true
- name: Generate coverage
run: flutter test --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: coverage/lcov.info
Scripts de Teste
# scripts/test.sh
#!/bin/bash
echo "Running unit tests..."
flutter test test/unit/ || exit 1
echo "Running widget tests..."
flutter test test/widget/ || exit 1
echo "Running integration tests..."
flutter test integration_test/ || exit 1
echo "All tests passed!"
📊 Cobertura de Testes
Meta de Cobertura
- Unit Tests: 80%+ (lógica de negócio crítica: 90%+)
- Widget Tests: 70%+ (componentes principais: 100%)
- Integration Tests: 50%+ (fluxos críticos: 100%)
Gerar Relatório de Cobertura
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
Excluir do Relatório
# lcov.info (editar manualmente ou usar ferramenta)
# Excluir arquivos gerados, modelos, etc.
✅ Boas Práticas
1. Nomenclatura
- Arquivos:
{feature}_{type}_test.dart(ex:feed_repository_test.dart) - Grupos:
group('FeatureName Tests', () { ... }) - Testes:
test('should do something when condition', () { ... })
2. Estrutura AAA
- Arrange: Preparar dados e mocks
- Act: Executar ação a ser testada
- Assert: Verificar resultado esperado
3. Isolamento
- Cada teste deve ser independente
- Usar
setUp()etearDown()para preparação/limpeza - Não depender de ordem de execução
4. Mocks
- Usar
mocktailoumockitopara mocks - Verificar chamadas com
verify() - Resetar mocks entre testes
5. Testes Mantíveis
- Nomes descritivos e claros
- Comentários quando necessário
- Helpers para código repetitivo
- Test data factories para dados de teste
6. Performance
- Unit tests devem ser rápidos (< 100ms)
- Widget tests devem ser moderados (< 1s)
- Integration tests podem ser mais lentos (< 30s)
Versão: 1.0
Última Atualização: 2025-01-20
Referências: