Flutter TDD Clean Architecture Course [2] – Entities & Use Cases

26  comments

In the first part, you learned the core concepts of clean architecture as it pertains to Flutter. We also created a bunch of empty folders for the presentation, domain and data layers inside the Number Trivia App we're building. Now it's time to start filling those empty folders with code, using TDD, of course.

TDD Clean Architecture Course
This post is just one part of a tutorial series. See all of the other parts here and learn to architect your Flutter apps!

Where to Start?

Whenever you are building an app with a UI, you should design the UI and UX first. I've done this homework for you and the app was showcased in the previous part.

The actual coding process will happen from the inner, most stable layers of the architecture outwards. This means we'll first implement the domain layer starting with the Entity. Before we do that though, we have to add certain package dependencies to pubspec.yaml. I don't want to bother with this file later, so let's fill in everything right now.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  # Service locator
  get_it: ^2.0.1
  # Bloc for state management
  flutter_bloc: ^0.21.0
  # Value equality
  equatable: ^0.4.0
  # Functional programming thingies
  dartz: ^0.8.6
  # Remote API
  connectivity: ^0.4.3+7
  http: ^0.12.0+2
  # Local cache
  shared_preferences: ^0.5.3+4

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^4.1.0

Entity

What kind of data will the Number Trivia App operate with? Well, NumberTrivia entities, of course. To find out which fields this class must have, we have to take a look at the response from the Numbers API. Our app will work with responses from concrete or random number URL, e.g http://numbersapi.com/42?json.

response.json

{
  "text": "42 is the answer to the Ultimate Question of Life, the Universe, and Everything.",
  "number": 42,
  "found": true,
  "type": "trivia"
}

We're interested only in the text and number fields. After all, the type will always be "trivia" in our case and the value of found is irrelevant. If a number is not found, we'll get the following response. It's still perfectly fine to display it in the app.

not_found.json

{
  "text": "123456 is an unremarkable number.",
  "number": 123456,
  "found": false,
  "type": "trivia"
}

NumberTrivia is one of the few classes which we aren't going to write in a test-driven way and it's for one simple reason - there's nothing to test. It extends Equatable to allow for easy value comparisons without all the boilerplate (which we'd have to test), since Dart supports only referential equality by default.

number_trivia.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

class NumberTrivia extends Equatable {
  final String text;
  final int number;

  NumberTrivia({
    @required this.text,
    @required this.number,
  }) : super([text, number]);
}

Use Cases

Use Cases are where the business logic gets executed. Sure, there won't be much logic in the Number Trivia App - all a UseCase will do is getting data from a Repository. We are going to have two of them - GetConcreteNumberTrivia and GetRandomNumberTrivia.

Data Flow & Error Handling

We know that Use Cases will obtain NumberTrivia entities from Repositories and they will pass these entities to the presentation layer. So, the type returned by a UseCase should be a Future<NumberTrivia> to allow for asynchrony, right?

Not so fast! What about errors? Is it the best choice to let exceptions freely propagate, having to remember to catch them somewhere else in the code? I don't think so. Instead, we want to catch exceptions as early as possible (in the Repository) and then return Failure objects from the methods in question.

Alright, let's recap. Repositories  and Use Cases will return both NumberTrivia and Failure objects from their methods. How is something like this possible? Please, enter functional programming.

The Either Type

The dartz package, which we've added as a dependency, brings functional programming (FP) to Dart. I won't pretend that I'm some FP pro, at least not yet. You don't need to know a lot of things either. All we're interested in for the purposes of better error handling is the Either<L, R> type.

This type can be used to represent any two types at the same time and it's just perfect for error handling, where L is the Failure and is the NumberTrivia. This way, the Failures don't have their own special "error flow" like exceptions do. They will get handled as any other data without using try/catch. Let's leave the details of how to work with Either for when we need it in the next parts of this course.

Defining Failures

Before we can proceed with writing the Use Cases, we have to define the Failures first, since they will be one part of the Either return type. Failures will be used across multiple app features and layers, so let's create them in the core folder under a new error subfolder.

Failures go into the error folder

There will be one base abstract Failure class from which any concrete failure will be derived, much like it is with regular exceptions and the base Exception class.

failures.dart

import 'package:equatable/equatable.dart';

abstract class Failure extends Equatable {
  // If the subclasses have some properties, they'll get passed to this constructor
  // so that Equatable can perform value comparison.
  Failure([List properties = const <dynamic>[]]) : super(properties);
}

This is enough for now, we will define some concrete Failures, like ServerFailure, in the next parts of this course.

Repository Contract

As you hopefully remember from the last part, and as is siginified on the diagram above, a Repository, from which the UseCase gets its data, belongs both to the domain and data layer. To be more precise, its definition (a.k.a. contract) is in domain, while the implementation is in data.

This allows for a total independence of the domain layer, but there is another benefit which we haven't talked about yet - testability. That's right! Testability and separation of concerns go together extremely well. Oh, the beauty of good architecture...

Writing a contract of the Repository, which in the case of Dart is an abstract class, will allow us to write tests (TDD style) for the UseCases without having an actual Repository implementation.

Testing without concrete implementation of classes is possible with mocking. A popular package for this is caled mockito, which we've added to our project as a dev_dependency.

So, how will the contract look like? It will have two methods - one for getting concrete trivia, another for getting random trivia and the return type of these methods is Future<Either<Failure, NumberTrivia>>, ensuring that error handling will go like a breeze!

number_trivia_repository.dart

import 'package:dartz/dartz.dart';

import '../../../../core/error/failure.dart';
import '../entities/number_trivia.dart';

abstract class NumberTriviaRepository {
  Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
  Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia();
}

GetConcreteNumberTrivia

Although this part is getting quite long and information-packed already, I don't want to leave you hanging. We're finally going to write some tests while implementing the GetConcreteNumberTrivia use case. In the next part, we will add the GetRandomNumberTrivia use case, so definitely stay tuned for that!

As is the case with TDD, we are going to write the test before writing the production code. This ensures that we won't add a bunch of things that we "ain't gonna need" and we'll also get the confidence that our code isn't going to fall apart like dominoes.

Writing the Test

In Dart apps, tests go into the test folder and it's a custom to make the test folders map the lib folders. Let's create all the root ones and also a folder called "usecases" under "domain".

The test we're about to write goes into the usecases folder

Create a new file under the "usecases" test folder called get_concrete_number_trivia_test.dart and while we're at it, also a get_concrete_number_trivia.dart under "usecases" lib folder.

Test files written using TDD always map to production files and append a "_test" at the end of their name.

Let's set up the test first. We know that the Use Case should get its data from the NumberTriviaRepository. We'll mock it, since we only have an abstract class for it and also because mocking allows us to check, among other things, if a method has been called.

get_concrete_number_trivia_test.dart

import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

To operate with this NumberTriviaRepository instance, the GetConcreteNumberTrivia use case will get it passed in through a constructor. Tests in Dart have a handy method called setUp which runs before every individual test. This is where we will instantiate the objects.

NOTE that the code we're writing will be full of errors - we don't even have a GetConcreteNumberTrivia class yet.

get_concrete_number_trivia_test.dart

import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

void main() {
  GetConcreteNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;

  setUp(() {
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
  });
}

Although we haven't really written any tests yet, now it's a good time to start writing the production code. We want to make a skeleton for the GetConcreteNumberTrivia class, so that the setup code above will be error-free.

get_concrete_number_trivia.dart

import '../repositories/number_trivia_repository.dart';

class GetConcreteNumberTrivia {
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);
}

Now comes the time to write the actual test. Since the nature of our Number Trivia App is simple, there won't be much logic in the Use Case, actually, no real logic at all. It will just get data from the Repository.

Therefore, the first and only test will ensure that the Repository is actually called and that the data simply passes unchanged throught the Use Case.

get_concrete_number_trivia_test.dart

import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

void main() {
  GetConcreteNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;

  setUp(() {
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
  });

  final tNumber = 1;
  final tNumberTrivia = NumberTrivia(number: 1, text: 'test');

  test(
    'should get trivia for the number from the repository',
    () async {
      // "On the fly" implementation of the Repository using the Mockito package.
      // When getConcreteNumberTrivia is called with any argument, always answer with
      // the Right "side" of Either containing a test NumberTrivia object.
      when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
          .thenAnswer((_) async => Right(tNumberTrivia));
      // The "act" phase of the test. Call the not-yet-existent method.
      final result = await usecase.execute(number: tNumber);
      // UseCase should simply return whatever was returned from the Repository
      expect(result, Right(tNumberTrivia));
      // Verify that the method has been called on the Repository
      verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
      // Only the above method should be called and nothing more.
      verifyNoMoreInteractions(mockNumberTriviaRepository);
    },
  );
}

When you think about it, the test above reads like documentation, even without all of my comments. Running the test now is pointless, since there are even compilation errors, so we can jump into implementation.

All we need to add to the GetConcreteNumberTrivia use case is the following function which will do everything prescribed by the test.

get_concrete_number_trivia.dart

import 'package:dartz/dartz.dart';
import 'package:meta/meta.dart';

import '../../../../core/error/failure.dart';
import '../entities/number_trivia.dart';
import '../repositories/number_trivia_repository.dart';

class GetConcreteNumberTrivia {
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);

  Future<Either<Failure, NumberTrivia>> execute({
    @required int number,
  }) async {
    return await repository.getConcreteNumberTrivia(number);
  }
}

When you now run the test (if you don't know how, refer to your IDE documentation), it's going to pass! And with that we've just written the first Use Case of the Number Trivia App using TDD. 

In the next part, we're going to refactor the above code, create a UseCase base class to make the app easily extendable and add a GetRandomNumberTrivia use case. Subscribe to the mailing list below to get notified about new tutorials and much more from the world of Flutter!

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪

You may also like

Flutter Bloc & Cubit Tutorial

Flutter Custom & Staggered Page Transition Animation Tutorial

    • We are going to be using RxDart indirectly through the flutter_bloc package. Dartz is here only for the Either type so that we can have a clean “error flow” without catching exceptions everywhere.

  • Hi. thanks for this very informative series and what you are doing for the community.
    I think the equatable package has changed and now the properties are taken via a getter instead of through the constructor. I’m using Equatable 0.6.1

  • Hi @Matej,

    dart-import Extension showing this error…
    And googling can’t fix my problem.

    Current file is not on project root or not on lib folder? File must be on $root/lib. Your current file path is: ‘d:Flutternumber_exclibfeaturesnumber_triviadomainrepositoriesnumber_trivia_repository.dart’ and the lib folder according to the pubspec.yaml file is ‘d:Flutternumber_exc/lib’.

  • Why doesn’t the entity have the id? For example: Suppose the data source is a sqflite database, how could I get a user’s data by id if the entity doesn’t take the user id into the widget?

  • Great work! I love your tutorials! Could you explain how you would implement additional features without code duplication and how you handle communication for shared data?

  • Hi Matej!

    First of all, thank you so much for such a great job! The tutorial is amazing and project results in a really clean and organized architecture.

    I’m implementing this with a simple UseCase of a SignIn. In my case, I’ve got a UserRepository with the following method (User is the entity):
    Future<Either> signIn(String email, String password);

    In the other hand my UseCase has its method execute returning the same: Future<Either>.

    However, when running my test the very first time, I’m getting error:
    Expected: Right:
    Actual: <Instance of 'Future<Either>’>

    Why is that? My code on the test is exactly the same as yours, but instead of a tNumber, I have a tEmail and tPassword. Could you please shed some light on this?

    Thank you so much!

    • Ok. For some reason the blog comments are getting parts of the comments as HTML tags I guess.

      The type of the Future is Either Failure or User.

      The error I’m getting is:
      Expected: Right[dynamic, User]:[Right(User)]
      Actual: [Instance of ‘Future[Either[Failure, User]]’]

      (I’ve replaced “lower than” and “greater than” with box brackets)

      • Forget it… I’m an idiot.. I forgot await keyword before usecase.execute(), that’s why I was getting a Future instead of the actual Right(User)

  • Thank you so much Matej for this serie I used to get in touch with Flutter!

    Now a few months after than you publication, Equatable is now at version 1.0.2, and it seems that the syntax used for the super() in the Failure abstract class is no longer supported.

    I’ve been struggling to fix that issue, but I’m not so experienced with Dart nor Equatable, and I couldn’t manage to adapt the code there…
    Any idea maybe ?

    • you need to remove the super part as we don’t rely on the superclass constructor and you have to implement props so the new class will be something like this:

      /////////////////////////////////////////////////////////////////////////////////////////
      import ‘package:equatable/equatable.dart’;
      import ‘package:meta/meta.dart’;

      class NumberTrivia extends Equatable {
      final String text;
      final int number;

      NumberTrivia({
      @required this.text,
      @required this.number,
      });

      @override
      List get props => [text, number];
      }

  • Hi,
    First of all, excellent series! Just loved it. Excited for your DDD series.

    My doubt is, that when I run these tests on Docker, the docker is unable to find the fixture files. Any idea why?

    (Although it runs the tests even though they are also right under the test directory.)

    • I have the same issue using json-files with Travis Ci and Github Actions. So I had to do a new class that returns objects of it instead. Works good but would rather find a better solution. Sound like it might be the same issue, have you found any solution?

    • Dhruvam please how to test on Docker, I need it now on my project so I have’nt make it. (I’m French so apologize for my english)

  • Hi Matt,
    I’m new to TDD

    Why did you overide the getConcreteNumberTrivia functionality, using when(..).thenAnswer() ?

    this will return the same result even when the implementation of the mock is not complete or return unexpected results, this always gives green indicator.

    I know I’m wrong, but unable to see how?
    thank

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >