Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

feat: add tests and mocking for flutter #25

Merged
merged 1 commit into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
/coverage/

# Symbolication related
app.*.symbols
Expand Down
7 changes: 7 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,10 @@ analyze:
# Generate code
generate:
flutter gen-l10n

# Coverage report
coverage:
#!/bin/bash
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
5 changes: 4 additions & 1 deletion lib/features/todos/todo_form_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ class TodoFormPage extends HookConsumerWidget {
final description = useState(todo?.description);

return Scaffold(
appBar: AppBar(title: Text(isNew ? 'New Todo' : 'Edit Todo')),
appBar: AppBar(
title: Text(
isNew ? Loc.of(context).createTodo : Loc.of(context).updateTodo),
),
body: Form(
child: Padding(
padding: const EdgeInsets.all(16),
Expand Down
2 changes: 1 addition & 1 deletion lib/features/todos/todos_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TodosPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(title: Text(Loc.of(context).todos)),
body: todos.isEmpty
? const Center(child: Text('No Todos'))
? Center(child: Text(Loc.of(context).noTodos))
: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
Expand Down
5 changes: 3 additions & 2 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
"save": "Save",

"todos": "Todos",
"createTodo": "Create Todo",
"updateTodo": "Update Todo",
"noTodos": "No todos",
"createTodo": "Create todo",
"updateTodo": "Update todo",
"titleRequired": "Title is required",

"youHavePushedTheButton": "You have pushed the button this many times:"
Expand Down
10 changes: 8 additions & 2 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,22 @@ abstract class Loc {
/// **'Todos'**
String get todos;

/// No description provided for @noTodos.
///
/// In en, this message translates to:
/// **'No todos'**
String get noTodos;

/// No description provided for @createTodo.
///
/// In en, this message translates to:
/// **'Create Todo'**
/// **'Create todo'**
String get createTodo;

/// No description provided for @updateTodo.
///
/// In en, this message translates to:
/// **'Update Todo'**
/// **'Update todo'**
String get updateTodo;

/// No description provided for @titleRequired.
Expand Down
7 changes: 5 additions & 2 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ class LocEn extends Loc {
String get todos => 'Todos';

@override
String get createTodo => 'Create Todo';
String get noTodos => 'No todos';

@override
String get updateTodo => 'Update Todo';
String get createTodo => 'Create todo';

@override
String get updateTodo => 'Update todo';

@override
String get titleRequired => 'Title is required';
Expand Down
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
mocktail:
dependency: "direct dev"
description:
name: mocktail
sha256: f603ebd85a576e5914870b02e5839fc5d0243b867bf710651cf239a28ebb365e
url: "https://pub.dev"
source: hosted
version: "1.0.2"
models:
dependency: "direct main"
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
mocktail: ^1.0.2

flutter:
uses-material-design: true
Expand Down
26 changes: 26 additions & 0 deletions test/features/app/app_tabs_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:flutter_starter/features/app/app_tabs.dart';
import 'package:flutter_starter/features/counter/counter_page.dart';
import 'package:flutter_starter/features/todos/todos_page.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../helpers/widget_helpers.dart';

void main() {
testWidgets('should start on CounterPage', (WidgetTester tester) async {
await tester.pumpWidget(
WidgetHelpers.testableWidget(child: const AppTabs()),
);

expect(find.byType(CounterPage), findsOneWidget);
});

testWidgets('should show TodosPage when tapped', (WidgetTester tester) async {
await tester.pumpWidget(
WidgetHelpers.testableWidget(child: const AppTabs()),
);

await tester.tap(find.text('Todos'));
await tester.pumpAndSettle();
expect(find.byType(TodosPage), findsOneWidget);
});
}
15 changes: 15 additions & 0 deletions test/features/app/app_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:flutter_starter/features/app/app.dart';
import 'package:flutter_starter/features/app/app_tabs.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../helpers/widget_helpers.dart';

void main() {
testWidgets('should show AppTabs', (WidgetTester tester) async {
await tester.pumpWidget(
WidgetHelpers.testableWidget(child: const App()),
);

expect(find.byType(AppTabs), findsOneWidget);
});
}
18 changes: 18 additions & 0 deletions test/features/counter/counter_page_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/counter/counter_page.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../helpers/widget_helpers.dart';

void main() {
testWidgets('should increment counter', (WidgetTester tester) async {
await tester.pumpWidget(
WidgetHelpers.testableWidget(child: const CounterPage()),
);

expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
}
73 changes: 73 additions & 0 deletions test/features/todos/todo_form_page_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/todos/todo_form_page.dart';
import 'package:flutter_starter/features/todos/todos_notifier.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:models/models.dart';

import '../../helpers/mocks.dart';
import '../../helpers/widget_helpers.dart';

void main() {
late MockTodosNotifier todosNotifier;

Widget testableWidget(Todo? todo) {
todosNotifier = MockTodosNotifier([]);
return WidgetHelpers.testableWidget(
child: TodoFormPage(todo: todo),
overrides: [
todosProvider.overrideWith(() => todosNotifier),
]);
}

setUpAll(() {
registerFallbackValue(Todo(
title: 'title',
description: 'description',
completed: false,
));
});

group('TodoFormPage', () {
testWidgets('should create new todo', (WidgetTester tester) async {
await tester.pumpWidget(testableWidget(null));

when(() => todosNotifier.add(any())).thenReturn(null);
expect(find.text('Create todo'), findsOneWidget);

await tester.enterText(find.byType(TextFormField).first, "title");
await tester.enterText(find.byType(TextFormField).last, "description");
await tester.tap(find.byType(FilledButton));
await tester.pumpAndSettle();

verify(() => todosNotifier.add(any())).called(1);
});

testWidgets('should update todo', (WidgetTester tester) async {
await tester.pumpWidget(
testableWidget(Todo(title: 'title', description: 'description')),
);

when(() => todosNotifier.update(any())).thenReturn(null);
expect(find.text('Update todo'), findsOneWidget);
expect(find.text('title'), findsOneWidget);
expect(find.text('description'), findsOneWidget);

await tester.tap(find.byType(FilledButton));
await tester.pumpAndSettle();

verify(() => todosNotifier.update(any())).called(1);
});

testWidgets('should show error with empty title', (tester) async {
await tester.pumpWidget(testableWidget(null));

expect(find.text('Create todo'), findsOneWidget);

await tester.tap(find.byType(FilledButton));
await tester.pumpAndSettle();

expect(find.text('Title is required'), findsOneWidget);
});
});
}
79 changes: 79 additions & 0 deletions test/features/todos/todos_notifier_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:flutter_starter/features/todos/todos_notifier.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mocktail/mocktail.dart';
import 'package:models/models.dart';
import 'package:services/services.dart';

import '../../helpers/mocks.dart';
import '../../helpers/riverpod_helpers.dart';

void main() {
group('TodosNotifer', () {
late MockTodosService todosService;
late ProviderContainer container;

setUp(() {
todosService = MockTodosService();
container = createContainer(overrides: [
todosServiceProvider.overrideWithValue(todosService),
]);
});

test('should have correct initial state', () {
when(() => todosService.todos).thenReturn([]);
expect(container.read(todosProvider), []);
});

test('should add todos', () {
final todo = Todo(
title: 'title',
description: 'description',
completed: false,
);

when(() => todosService.add(todo)).thenReturn(null);
when(() => todosService.todos).thenReturn([todo]);

container.read(todosProvider.notifier).add(todo);

verify(() => todosService.add(todo)).called(1);

expect(container.read(todosProvider), [todo]);
});

test('should update todos', () {
final todo = Todo(
title: 'title',
description: 'description',
completed: false,
);

when(() => todosService.update(todo)).thenReturn(null);
when(() => todosService.todos).thenReturn([todo]);

container.read(todosProvider.notifier).update(todo);

verify(() => todosService.update(todo)).called(1);

expect(container.read(todosProvider), [todo]);
});

test('should remove todos', () {
final todo = Todo(
title: 'title',
description: 'description',
completed: false,
);

when(() => todosService.remove(todo)).thenReturn(null);
when(() => todosService.todos).thenReturn([todo]);

container.read(todosProvider.notifier).remove(todo);

verify(() => todosService.remove(todo)).called(1);

expect(container.read(todosProvider), [todo]);
});
});
}
Loading