2

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

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!

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

  • Excellent understanding Matej.. Just one concern with regards to using dartz package vs RxDart. Can RxDart be used instead of Dartz or is there any particular reason we are going with Dartz.

    • 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.

  • >