{"id":25043004,"url":"https://github.com/jackwill99/test-driven-development-flutter","last_synced_at":"2025-09-03T22:33:30.277Z","repository":{"id":191792126,"uuid":"685391347","full_name":"jackwill99/test-driven-development-flutter","owner":"jackwill99","description":"Test Driven Development for flutter","archived":false,"fork":false,"pushed_at":"2023-09-04T16:36:00.000Z","size":94,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-30T23:27:32.074Z","etag":null,"topics":["flutter-clean-architecture","flutter-unit-testing"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jackwill99.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-31T06:01:46.000Z","updated_at":"2024-06-03T20:16:23.000Z","dependencies_parsed_at":"2025-03-30T23:34:24.485Z","dependency_job_id":null,"html_url":"https://github.com/jackwill99/test-driven-development-flutter","commit_stats":null,"previous_names":["jackwill99/test-driven-development-flutter"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jackwill99/test-driven-development-flutter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackwill99%2Ftest-driven-development-flutter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackwill99%2Ftest-driven-development-flutter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackwill99%2Ftest-driven-development-flutter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackwill99%2Ftest-driven-development-flutter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jackwill99","download_url":"https://codeload.github.com/jackwill99/test-driven-development-flutter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackwill99%2Ftest-driven-development-flutter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273523060,"owners_count":25120859,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-03T02:00:09.631Z","response_time":76,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flutter-clean-architecture","flutter-unit-testing"],"created_at":"2025-02-06T04:52:21.322Z","updated_at":"2025-09-03T22:33:30.255Z","avatar_url":"https://github.com/jackwill99.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Test Driven Development\n\nIn that `clean architecture`, there has `three layers`\n\n- ## Domain\n    - Entity\n    - UseCases\n    - Repository\n- ## Data\n    - Repository\n    - DataSources\n    - Model\n- ## Presentation\n    - Pages\n    - StateManagement\n    - Widgets\n\n![CleanArchitecture](readme-img/clean-arch.png)\n\nWe have to start from `the domain layer.` The main is the inner layer and which shouldn't be\nsusceptible and will contain\nonly `The Core Business logic and business objects (Enterprise/System-wide business rules)`. The\ndomain layer should be `totally independent of every other layer`. It can make changes/additions\nwithout interfering with rest of program even though it is a core layer.\n\nLet's create the `Entity of the Domain layer`.\n\n### Entity\n\n**`Entity are business objects of an applications or a system - eg. Article, Blog,`**\n\n```dart\n// domain/entities/promo.dart\nimport 'package:equatable/equatable.dart';\n\n// Abstract Business object\nabstract class Promo extends Equatable {\n  final String eventName;\n  final String poster;\n  final DateTime duration;\n\n  const Promo({\n    required this.eventName,\n    required this.poster,\n    required this.duration,\n  });\n\n  @override\n  List\u003cObject?\u003e get props =\u003e [eventName, poster, duration];\n}\n\n```\n\n### Repository\n\nRepository is the bridge between `Domain layer` and `Data layer`. Actual implementations of\nrepositories is in the `Data layer`. Repositories are responsible for coordinating data from the\ndifferent data sources. The repository of the domain layer is in the form of an abstract class in\nwhich there are functions that to be implemented.\n\nYou can also called `Business Logic method` and we will test it later.\n\n```dart\n// domain/repositories/promo_repository.dart\n\nimport '../entities/promo.dart';\n\n// Abstract Business logic methods\nabstract class PromoRepository {\n  Future\u003cPromo\u003e getFavPromo();\n\n  Future\u003cPromo\u003e getExpiryPromo();\n\n  Future\u003cvoid\u003e saveFavPromo({required Promo promo});\n\n  Future\u003cvoid\u003e delFavPromo({required Promo promo});\n}\n\n```\n\nIn the `repository of the Data Layer` that we will start implementing the abstract classes\nof `repository of the Domain Layer`.\n\n```dart\n// data/repositories/promo_repository_impl.dart\nimport 'dart:async';\n\nimport '../../domain/entities/promo.dart';\nimport '../../domain/repositories/promo_repository.dart';\nimport '../data_sources/api_service.dart';\nimport '../models/promo_model.dart';\n\n// Implemented Business logic methods\nclass PromoRepositoryImpl implements PromoRepository {\n\n  @override\n  Future\u003cvoid\u003e delFavPromo({required Promo promo}) async {\n    throw UnimplementedError();\n  }\n\n  @override\n  Future\u003cvoid\u003e saveFavPromo({required Promo promo}) async {\n    throw UnimplementedError();\n  }\n\n  @override\n  Future\u003cPromoModel\u003e getExpiryPromo() async {\n    throw UnimplementedError();\n  }\n\n  @override\n  Future\u003cPromoModel\u003e getFavPromo() async {\n    throw UnimplementedError();\n  }\n}\n\n\n```\n\n## Model\n\nIn the implementing of repository, we need the response model. So, *let create promo model as the\nsame of promo entity.*\n\n```dart\n// data/models/promo_model.dart\nimport '../../domain/entities/promo.dart';\n\n// Business object model\nclass PromoModel extends Promo {\n  const PromoModel({\n    required super.eventName,\n    required super.poster,\n    required super.duration,\n  });\n}\n\n\n```\n\n## Models Vs Entities\n\n**`Why do we need a model and not use entity?`**\n\nBecause as we said before, the domain layer `must be independent` and not depend on other layers.\nExample, when we want to change our database in the future data type or adding new properties and we\nhave to change the entity if\nwe use entity instead of model in the Data Layer. That is against the rule of Clean Architecture.\n\n## Data Sources\n\n`Service that provides the data we need like API service or Persistence service.`\n\n```dart\n// data/data_sources/api_service.dart\n\nimport 'package:flutter/foundation.dart';\n\nimport '../../domain/entities/promo.dart';\nimport '../models/promo_model.dart';\n\n// Data Service to provide data from api source or persistence source or somewhere\nclass ApiService {\n  Future\u003cvoid\u003e delFavPromo({required Promo promo}) async {\n    await Future.delayed(const Duration(seconds: 10));\n    if (kDebugMode) {\n      print(\"del Fav Promo pass\");\n    }\n  }\n\n  Future\u003cvoid\u003e saveFavPromo({required Promo promo}) async {\n    await Future.delayed(const Duration(seconds: 10));\n    if (kDebugMode) {\n      print(\"save Fav Promo pass\");\n    }\n  }\n\n  Future\u003cPromoModel?\u003e getExpiryPromo() async {\n    await Future.delayed(const Duration(seconds: 10));\n    if (kDebugMode) {\n      print(\"get Expiry Promo pass\");\n    }\n    return PromoModel(\n      eventName: \"eventName\",\n      poster: \"promo\",\n      duration: DateTime.now(),\n    );\n  }\n\n  Future\u003cPromoModel?\u003e getFavPromo() async {\n    await Future.delayed(const Duration(seconds: 10));\n    if (kDebugMode) {\n      print(\"get Fav Promo pass\");\n    }\n    return PromoModel(\n      eventName: \"expiryEventName\",\n      poster: \"expiryPromo\",\n      duration: DateTime.now(),\n    );\n  }\n}\n\n\n```\n\nAnd, use this data provided service in `implemented repository of the Data Layer` and combine with\nyour business logic.\n\n```dart\n// data/repositories/promo_repository_impl.dart\n\nimport 'dart:async';\n\nimport '../../domain/entities/promo.dart';\nimport '../../domain/repositories/promo_repository.dart';\nimport '../data_sources/api_service.dart';\nimport '../models/promo_model.dart';\n\n// Implemented Business logic methods\nclass PromoRepositoryImpl implements PromoRepository {\n  late ApiService _apiServices;\n\n  PromoRepositoryImpl() {\n    _apiServices = ApiService();\n  }\n\n  @override\n  Future\u003cvoid\u003e delFavPromo({required Promo promo}) async {\n    _apiServices.delFavPromo(promo: promo);\n  }\n\n  @override\n  Future\u003cvoid\u003e saveFavPromo({required Promo promo}) async {\n    _apiServices.saveFavPromo(promo: promo);\n  }\n\n  @override\n  Future\u003cPromoModel\u003e getExpiryPromo() async {\n    final promo = await _apiServices.getExpiryPromo();\n    if (promo == null) {\n      throw \"There is an error to get expiry promo\";\n    }\n    return promo;\n  }\n\n  @override\n  Future\u003cPromoModel\u003e getFavPromo() async {\n    final promo = await _apiServices.getFavPromo();\n    if (promo == null) {\n      throw \"There is an error to get Fav promo\";\n    }\n    return promo;\n  }\n}\n\n```\n\n## Use cases\n\n`Use cases are where the business logic(implemented repository of the Data Layer) gets executed.`\n\nAll of a use case will do that is getting data from a repository and returns it.\nSo we define an instance of the repository and set it in the class constructor.\nThen by calling method, we get the data from the repository and return it.\n\n```dart\n// domain/usecases/promo_usecase.dart\n\nimport '../entities/promo.dart';\nimport '../repositories/promo_repository.dart';\n\nclass PromoUseCase {\n  final PromoRepository _promoRepository;\n\n  PromoUseCase(this._promoRepository);\n\n  Future\u003cPromo\u003e getFavPromo() async {\n    return _promoRepository.getFavPromo();\n  }\n\n  Future\u003cPromo\u003e getExpiryPromo() async {\n    return _promoRepository.getExpiryPromo();\n  }\n\n  Future\u003cvoid\u003e saveFavPromo({required Promo promo}) async {\n    _promoRepository.saveFavPromo(promo: promo);\n  }\n\n  Future\u003cvoid\u003e delFavPromo({required Promo promo}) async {\n    _promoRepository.delFavPromo(promo: promo);\n  }\n}\n\n```\n\n## Presentation\n\nI won't say anymore about pages and widgets. The main topic is about the `state`. You can use any\nstate management of the presentation layer. Here, we will use `Getx State Control`.\n\nWe will control all of `Business logic of the business object model` in the controller and they are\nalready executed in the business use-case. So in the controller, **we will assign Business use-case\nwhen we inject our controller.**\n\n```dart\n// presentation/state/promo_controller.dart\n\nimport 'package:get/get.dart';\n\nimport '../../data/repositories/promo_repository_impl.dart';\nimport '../../domain/entities/promo.dart';\nimport '../../domain/usecases/promo_usecase.dart';\n\nclass PromoController extends GetxController {\n  late PromoUseCase _promoUseCase;\n\n  @override\n  onInit() {\n    super.onInit();\n    _promoUseCase = PromoUseCase(PromoRepositoryImpl());\n  }\n\n  Future\u003cPromo\u003e getFavPromo() async {\n    return _promoUseCase.getFavPromo();\n  }\n\n  Future\u003cPromo\u003e getExpiryPromo() async {\n    return _promoUseCase.getExpiryPromo();\n  }\n\n  Future\u003cvoid\u003e saveFavPromo({required Promo promo}) async {\n    _promoUseCase.saveFavPromo(promo: promo);\n  }\n\n  Future\u003cvoid\u003e delFavPromo({required Promo promo}) async {\n    _promoUseCase.delFavPromo(promo: promo);\n  }\n}\n\n```\n\nIn main.dart, we will inject the controller and execute when we click the floating button.\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:get/get_navigation/src/root/get_material_app.dart';\nimport 'package:get/instance_manager.dart';\nimport 'package:test_driven_development/promo/data/models/promo_model.dart';\nimport 'package:test_driven_development/promo/presentation/state/promo_controller.dart';\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  MyApp({super.key});\n\n  final promoUsecase = Get.put(PromoController());\n\n  @override\n  Widget build(BuildContext context) {\n    return GetMaterialApp(\n      color: Colors.black,\n      initialRoute: \"/\",\n      home: Scaffold(\n        body: const Center(\n          child: Text(\"Example of Clean Architecture\"),\n        ),\n        floatingActionButton: FloatingActionButton(\n          onPressed: () {\n            promoUsecase.saveFavPromo(\n              promo: PromoModel(\n                duration: DateTime.now(),\n                eventName: \"eventName\",\n                poster: 'poster',\n              ),\n            );\n          },\n          child: const Icon(Icons.add),\n        ),\n      ),\n    );\n  }\n}\n\n```\n\n*Here is a quote from the\nbook **Clean Architecture: A Craftsman’s Guide to Software Structure and Design** :*\n\n`The architect can employ the Single Responsibility Principle and the Common Closure Principle to separate those things that change for different reasons, and to collect those things that change for the same reasons—given the context of the intent of the system.\nUser interfaces change for reasons that have nothing to do with business rules. Business rules themselves may be closely tied to the application, or they may be more general. The database, the query language, and even the schema are technical details that have nothing to do with the business rules or the UI.\nThus we find the system divided into decoupled horizontal layers— the application-independent business rules, Application-specific business rules, UI, and the Database.`\n\nLater in the book **Robert Martin** describes in detail how to build these 4 layers:\nentities \u003c- use-cases \u003c- interface adapters \u003c- frameworks and drivers.\n\n## Testing\n\nFor testing, you should learn how to unit test in flutter\nand then [here](https://dev.to/infiniteoverflow/a-comprehensive-guide-to-mockito-in-flutter-1od0)\nwill discuss about mock for testing.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackwill99%2Ftest-driven-development-flutter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjackwill99%2Ftest-driven-development-flutter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackwill99%2Ftest-driven-development-flutter/lists"}