Part I here
Part III here
Part IV here
Welcome back to the Part II of this series. Let’s continue from where we left. Here we will begin to code and finish TDD for the domain layer.
TDD Clean Architecture
Now we know a little bit about TDD and Clean Architecture, let us combine those two to get the best of both worlds. Before jumping onwards here are a few things :
- We will be writing a unit test.
- We will be using Mocktail to mock the dependencies.
class MockAwsDataSource extends Mock implements AwsDataSource {}
// To mock class AwsDataSourece using mocktail
- While testing any class, mock all the classes/dependencies inside it except the class we are testing and control all the functions inside those classes.
- You should have a similar folder structure inside the test folder i.e. mimic the lib folder structure inside the test folder.
You should have basic knowledge on dart and abstraction
Domain Layer
It is one of the most important layers. It should remain unchanged even if all other layer changes. We should always start with the domain layer.
The domain layer consists of entities, repositories, and use cases.
Entities
It is a lightweight domain object and the basic data structure that we will absolutely need.
Eg. Entity of Movie lib/features/movie/domain/entities/movie_entity.dart
import 'package:equatable/equatable.dart';
class MovieEntity extends Equatable {
final String movieId;
final String title;
final String thumbnail;
final String movieUrl;
final bool unlocked;
const MovieEntity({
required this.movieId,
required this.title,
required this.thumbnail,
required this.movieUrl,
required this.unlocked,
});
@override
List<Object> get props {
return [
movieId,
title,
thumbnail,
movieUrl,
unlocked,
];
}
}
Equatable overrides == and hashCode for you so you don't have to waste your time writing lots of boilerplate code.
Repositories
It is in both the domain and data layer. Its definition is in the domain layer and implementation is in the data layer allowing total independence of the domain layer.
Eg. Repository of Movie lib/features/movie/domain/repositories/movie_repository.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
import '../entities/movie_entity.dart';
abstract class MovieRepository {
Future<Either<Failure, List<MovieEntity>>> getMovieList();
}
It will have a method to get a list of movies, i.e. list of movie entities. Future>> will ensure error handling . Let us move on. Wait a minute what is either keyword? It allows a function to return between failure and List of movie entities, not both. It has been obtained from package Dartz which gives functional programming capabilities to dart. Failure class was defined in the core/error file to define the failure of the code.
In core the features, abstraction or module that is used in other features are defined
Eg. Failure is defined in lib/core/error/failure.dart
import 'package:equatable/equatable.dart';
abstract class Failure extends Equatable {
@override
List<Object?> get props => [];
}
//General Failures
class ServerFailure extends Failure {}
class CacheFailure extends Failure {}
Usecases
Use Cases are where the business logic gets executed. It calls a repository to get data.
Eg. Usecase to get movie list lib/features/movie/domain/usecases/get_movie_list_usecase.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/movie_entity.dart';
import '../repositories/movie_repository.dart';
class GetMovieListUsecase implements UseCase<List<MovieEntity>, NoParams> {
final MovieRepository movieRepository;
GetMovieListUsecase({
required this.movieRepository,
});
@override
Future<Either<Failure, List<MovieEntity>>> call(NoParams params) async {
return await movieRepository.getMovieList();
}
}
GetVideoLiseUsecase implements use-case inside lib/core/use-case . We are defining rules for use cases there.
Eg. Base use-case is defined in lib/core/usecases/usecase.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import '../error/failure.dart';
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
class NoParams extends Equatable {
@override
List<Object?> get props => [];
}
Wait a minute this was supposed to be TDD Clean Architecture. Shouldn’t we write a test first?
Yes, before writing a use case, we should first write the test first. Just imagine the above code hasn’t been written yet for now and let’s write a test.
Test file should end with ‘_test.dart’ and you can run test by typing flutter test in terminal or you can run them manually clicking run. While naming the test give it the same name as the file you are trying to test and add ‘ _test ’ to it
Eg. Test for use-case test/features/movie/domain/usecases/get_movie_list_usecase_test.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockMovieRepository extends Mock implements MovieRepository {}
void main() {
late GetMovieListUsecase getMovieListUsecase;
late MockMovieRepository mockMovieRepository;
final tMovieList = [
const MovieEntity(
movieId: 'movieId',title: 'title',thumbnail: 'thumbnail',
movieUrl: 'movieUrl',unlocked: true,
),
const MovieEntity(
movieId: 'movieIds',title: 'titles',thumbnail: 'thumbnails',
movieUrl: 'movieUrls',unlocked: false,
)
];
setUp(() {
mockMovieRepository = MockMovieRepository();
getMovieListUsecase =
GetMovieListUsecase(movieRepository: mockMovieRepository);
});
test(
'should get list of movie',
() async {
// arrange
when(() => mockMovieRepository.getMovieList())
.thenAnswer((_) async => Right(tMovieList));
// act
final result = await getMovieListUsecase(NoParams());
// assert
verify(() => mockMovieRepository.getMovieList());
expect(result, Right(tMovieList));
verifyNoMoreInteractions(mockMovieRepository);
},
);
}
Now let’s explain the code
We are testing GetMovieListUsecase in get_movie_list_usecase.dart which implements a base use case in lib/core/usecases/usecase.dart . GetMovieListUsecase use case calls a repository for data, so the repository must be mocked to test the use case. After mocking the repository, we write the test.
Now just like in the original dart file, there is the main file that will be executed. The main function contains the test.
First, we define all the dependencies we need.
Then we instantiate them in setUp. If we look at the test there are three parts.
In arrange we control the outcome of the functions.
In act, we run the functions.
In assert, we verify/check if the test is successful or not. verify function is making sure that the function has been executed. expect to compare the result and verifyNoMoreInteractions to ensure that the repository is no longer being used or interacted with.
If we look closely, we can see that the test looks like documentation explaining to us what the code is supposed to do.
You might be wondering if usecase hasn’t been created yet how do we map or visualize the test. Here comes, abstraction to the rescue . Even though getMovieListUsecase hasn’t been created , the usecase has been defined using abstract class. We can use this abstract class to visualize what the usecase class will be like and use it to write the test(most of the time). So interface/abstract class are very important in TDD. After writing the test, we write the usecase until the test in successful and refactor it as needed.
Only use-case needs to be tested in the domain layer as the repository and entity have nothing to test as they are very simple.
Finally, we have the following file and folder
Domain Layer in lib |
Domain Layer in test |
In my next post, we will implement TDD for the Data layer.
Top comments (0)