You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
flutter test# Rodar todos os testes
flutter test test/helpers/ # Rodar pasta específica
flutter test test/helpers/taxes_test.dart # Rodar arquivo específico
flutter test --coverage # Com coverage
flutter test --reporter expanded # Output detalhado
dart run build_runner build # Gerar mocks (mockito)
dart run build_runner watch # Gerar mocks em modo watch
Estrutura de Pastas
banco_douro_app/
lib/
helpers/helper_taxes.dart
models/account.dart
providers/account_provider.dart
ui/screens/login_screen.dart
test/
helpers/
helper_taxes_test.dart ← Testes de unidade
models/
account_test.dart ← Testes de unidade
providers/
account_provider_test.dart ← Testes com mock
ui/
login_screen_test.dart ← Testes de widget
mocks/
mocks.dart ← @GenerateMocks (fonte)
mocks.mocks.dart ← Gerado por build_runner
group('MinhaClasse', () {
lateMinhaClasse instancia;
setUp(() { // Antes de CADA teste
instancia =MinhaClasse();
});
tearDown(() { // Depois de CADA teste
instancia.dispose();
});
setUpAll(() async { // UMA VEZ antes de todosawaitinitDB();
});
tearDownAll(() async { // UMA VEZ depois de todosawaitcloseDB();
});
test('...', () { ... });
});
find.text('Olá') // Por texto
find.byType(TextField) // Por tipo de widget
find.byKey(Key('minha-chave')) // Por Key
find.byIcon(Icons.add) // Por ícone
find.byWidget(meuWidget) // Por instância
find.ancestor(of: x, matching: y) // Ancestral
find.descendant(of: x, matching: y) // Descendente
async em testes
// Função assíncronatest('carrega dados', () async {
await provider.loadAll();
expect(provider.accounts, isNotEmpty);
});
// Widget com FuturetestWidgets('exibe loading e depois dados', (tester) async {
await tester.pumpWidget(MeuWidget());
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pumpAndSettle(); // espera o Future completarexpect(find.text('Dados carregados'), findsOneWidget);
});
App Inicia
|
v
Verifica token no SharedPreferences
|
+---> Token existe? ---> HomeScreen
|
+---> Não existe? ---> LoginScreen
|
v
Usuário digita email/senha
|
v
POST /login
|
+---------------+---------------+
| | |
v v v
Sucesso Usuário não Erro
| encontrado |
v | v
Salvar token Oferecer Mostrar erro
| registro
v |
HomeScreen POST /register
|
v
Salvar token
|
v
HomeScreen
Fluxo de Requisição Autenticada
Ação do Usuário (ex: editar conta)
|
v
Recuperar token do SharedPreferences
|
v
Montar headers com Authorization
|
v
Fazer requisição (PUT/DELETE)
|
+---> 200: Sucesso --> Atualizar UI
|
+---> 401: Não autorizado --> Verificar token
| |
| Token expirado?
| |
| Logout e ir para Login
|
+---> 4xx/5xx: Erro --> Mostrar mensagem
Scaffold() // Estrutura da telaAppBar() // Barra superiorCard() // Cartao com elevacaoElevatedButton() // Botao elevadoTextButton() // Botao de textoIconButton() // Botao de iconeListTile() // Item de listaDrawer() // Menu lateralBottomNavigationBar() // Barra inferiorFloatingActionButton() // Botao flutuante
final theme =Theme.of(context);
final colors = theme.colorScheme;
final textTheme = theme.textTheme;
Text('Titulo', style: theme.textTheme.headlineLarge);
Consumer<AccountProvider>(
builder: (context, p, _) {
if (p.isLoading) returnconstCircularProgressIndicator();
if (p.error !=null) returnText(p.error!);
if (p.accounts.isEmpty) returnconstText('Sem contas');
returnListView.builder(
itemCount: p.accounts.length,
itemBuilder: (_, i) =>AccountWidget(account: p.accounts[i]),
);
},
)
Acao no modal sem callback
// Modal NAO precisa de onSaved callbackElevatedButton(
onPressed: () async {
await context.read<AccountProvider>().addAccount(newAccount);
if (context.mounted) Navigator.pop(context);
// Consumer no pai ja atualiza automaticamente
},
)
// Opção A: StatefulWidget com initState (HomeScreen)class_HomeScreenStateextendsState<HomeScreen> {
@overridevoidinitState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<AccountProvider>().initialize();
});
}
}
// Opção B: disparar no LoginScreen antes de navegar (DashboardScreen)if (success) {
context.read<AccountProvider>().initialize(); // dispara sem bloquearNavigator.pushReplacementNamed(context, 'dashboard');
}
Comparativo de Gerenciadores
Gerenciador
Pkg
Curva
Quando usar
setState
-
Baixa
Estado local, 1 widget
Provider
provider
Baixa
Apps medios, equipes pequenas
Riverpod
flutter_riverpod
Media
Provider moderno, testavel
Cubit/BLoC
flutter_bloc
Alta
Enterprise, auditoria de estados
Redux
flutter_redux
Alta
Background JS/Redux, auditoria total
MobX
flutter_mobx
Media
Background JS/MobX, valores derivados
GetX
get
Baixa
Prototipagem rapida, solo
Qual usar?
Estado de um widget apenas?
└─► setState
Compartilhado entre 2+ telas, time pequeno?
└─► Provider ← esta aula
Provider dominado? Quer mais poder sem reescrever?
└─► Riverpod
Equipe grande, regras complexas, rastreabilidade?
└─► BLoC / Cubit
Background JS/Redux no time?
└─► Redux
Background JS/MobX ou muitos valores derivados?
└─► MobX
// ERRADO: a UI nao sabe que mudouvoidaddItem(Account a) {
_accounts.add(a); // modifica sem notificar
}
// CORRETOvoidaddItem(Account a) {
_accounts = [..._accounts, a];
notifyListeners();
}
Usar context apos await sem checar mounted
// ERRADO: pode crashar se widget foi desmontadoawait context.read<AccountProvider>().loadAll();
Navigator.pop(context); // pode ser inválido!// CORRETOawait context.read<AccountProvider>().loadAll();
if (context.mounted) Navigator.pop(context);
Criar provider dentro de build()
// ERRADO: nova instancia a cada rebuildWidgetbuild(BuildContext context) {
returnChangeNotifierProvider(
create: (_) =>AccountProvider(), // cria toda vez!
child: ...,
);
}
// CORRETO: provider criado fora do build (no main.dart ou acima na arvore)
// pump() — processa UM frameawait tester.pump();
// pump(Duration) — processa frames por X tempoawait tester.pump(constDuration(milliseconds:500));
// pumpAndSettle() — aguarda até TODAS as animações e futures terminaremawait tester.pumpAndSettle();
// pumpAndSettle com timeout (para operações de rede)await tester.pumpAndSettle(constDuration(seconds:10));
Quando usar cada um:
Situação
Use
Verificar estado intermediário
pump()
Aguardar animação de 300ms
pump(Duration(milliseconds: 300))
Após tap em botão com navegação
pumpAndSettle(Duration(seconds: 5))
Após operação de rede
pumpAndSettle(Duration(seconds: 10))
Verificar loading spinner
pump() antes do pumpAndSettle()
Finders
find.byKey(constKey('id')) // Por Key — mais estável
find.text('Entrar') // Por texto exato
find.textContaining('Olá') // Por texto parcial
find.byType(TextField) // Por tipo de widget
find.byIcon(Icons.add) // Por ícone
find.byWidget(meuWidget) // Por instância// Combinando finders
find.descendant(
of: find.byKey(constKey('form')),
matching: find.byType(TextFormField),
)
find.ancestor(
of: find.text('Salvar'),
matching: find.byType(ElevatedButton),
)
RED → GREEN → REFACTOR
1. Escrever teste que falha (RED)
2. Implementar o mínimo para passar (GREEN)
3. Melhorar sem quebrar os testes (REFACTOR)
4. Repetir
// RED: teste primeirotest('deve lançar exceção quando saldo insuficiente', () {
expect(() =>validateTransaction(...), throwsA(isA<InsufficientFundsException>()));
});
// GREEN: implementação mínimavoidvalidateTransaction(...) {
if (sender.balance < amount) throwInsufficientFundsException('Saldo insuficiente');
}
// REFACTOR: melhorar a mensagem e adicionar validações extras
BDD — Behavior-Driven Development
Given [estado inicial]
When [ação do usuário]
Then [resultado esperado]
// Nomenclatura BDD nos testes Fluttergroup('Dado que o usuário está logado', () {
group('Quando ele adiciona uma nova conta', () {
testWidgets('Então a conta aparece na lista', (tester) async {
// ...
});
});
});
DDD — Domain-Driven Design
Testar por camada:
Domínio (models, helpers): → Testes de unidade puros
Repositórios (SQLite, HTTP): → Testes com mocks
Providers (estado): → Testes com mocks
UI (telas): → Testes de widget
Fluxo completo: → Testes de integração
Priorização de Testes
Alta prioridade (sempre testar):
✅ Cálculos financeiros e de negócio
✅ Validações críticas (saldo insuficiente, campos obrigatórios)
✅ Fluxos de autenticação
✅ Operações que alteram dados (criar, editar, deletar)
Média prioridade:
✅ Telas principais (login, dashboard)
✅ Estados de loading e erro
✅ Fallback para dados locais
Baixa prioridade:
⚠️ Animações decorativas
⚠️ Cores e estilos
⚠️ Textos estáticos sem lógica
// ✅ Use Keys em vez de textos dinâmicos
find.byKey(constKey('loginButton')) // estável
find.text('Entrar') // frágil se o texto mudar// ✅ Timeout generoso para operações de redeawait tester.pumpAndSettle(constDuration(seconds:10));
// ✅ Cada teste é autossuficiente (não depende de outros)// No setUp ou início de cada testWidgets, comece do zero// ✅ Limpe dados entre testes quando necessáriosetUp(() async {
awaitDatabaseHelper.instance.clearAll();
});
// ✅ Prefira scrollUntilVisible a assumir posição do itemawait tester.scrollUntilVisible(find.text('Item distante'), 100);