Part I here
Part II here
Part IV here
In this part, let’s finish the data layer.
Data Layer
It is the layer in which the app interacts with the outside world. It provides data to the app from the outside. It consists of low-level Data sources, Repositories which are the single source of truth for the data, and finally Models.
Models
Models are the entity with some additional functionalities. The data from the outside mightn’t be in the format we desired. So, models interact with this harsh environment and convert the outside data to the entity. As the relationship between model and entity is very important, we should test if the models return the entity.
Eg. Movie Model Test test/features/movie/data/models/movie_model_test.dart
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:movie_show/features/movie/data/models/movie_model.dart';
import 'package:movie_show/features/movie/domain/entities/movie_entity.dart';
import '../../../../fixtures/fixture_reader.dart';
void main() {
const tMovieModel = MovieModel(
movieId:
'''Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttp://[www.bigbuckbunny.org'''](http://www.bigbuckbunny.org'''),
title: 'Big Buck Bunny',
thumbnail: 'images/BigBuckBunny.jpg',
movieUrl:
"[http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4](http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4)",
unlocked: true,
);
setUp(() {});
test('MovieModel should be subclass of MovieEntity', () async {
//arrange
//act
//assert
expect(tMovieModel, isA<MovieEntity>());
});
group('fromJson', () {
test('should return a valid MovieModel', () async {
//Arrange
final Map<String, dynamic> jsonMap =
json.decode(await fixture('video.json'));
//Act
final result = MovieModel.fromJson(jsonMap);
//Assert
expect(result, tMovieModel);
});
});
group('toJson', () {
test('should return a JSON map from MovieModel', () async {
//Arrange
//Act
final result = tMovieModel.toJson();
//Assert
final expectedMap = {
"movieId":
"Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttp://[www.bigbuckbunny.org](http://www.bigbuckbunny.org)",
"movieUrl":
"[http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4](http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4)",
"thumbnail": "images/BigBuckBunny.jpg",
"title": "Big Buck Bunny",
"unlocked": true
};
expect(result, expectedMap);
});
});
}
Json file has been used from here. You might be thinking why is movieID so long. It doesn’t matter here as we are just trying to learn here.
Here, we are writing multiple tests. A test can also be grouped together using a group.
At first, we make sure that the movie model extends from the movie entity and it returns the entity. Here we can see isA() keyword. It is used to check the type of object.
As our model interacts with the outside world and most of the data we get from the outside is in form of JSON, our model will have a function to convert this data to an entity. So, let’s write a test for fromjson and tojson.
As we are in the testing phase, let’s mimic those JSON responses.
Eg. Json responses test/fixtures/fixture_reader.dart
import 'dart:io';
Future<String> fixture(String name) {
return Future.value(File('test/fixtures/$name').readAsStringSync());
}
Fixture reads from the JSON that we have created and returns them. Let’s look at our JSON file.
Eg. JSON file. test/fixtures/video.json
{
"description": "Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttp://[www.bigbuckbunny.org](http://www.bigbuckbunny.org)",
"sources": [
"[http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4](http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4)"
],
"subtitle": "By Blender Foundation",
"thumb": "images/BigBuckBunny.jpg",
"title": "Big Buck Bunny",
"unlocked": true
}
In test/fixtures/video_list.json Copy json file from here
Wait a minute something seems wrong. It is not in the format where we defined the entity.
Should we go back and change the entity?
No, an entity is a rule that we cannot change (actually we can but we shouldn’t). The model is in charge of handling it, so the entity can get what it wants. If we run this test, the test fails (obviously as the model hasn’t been created yet), so let us write the model to make it pass.
Eg. Entity of Movie lib/features/movie/data/models/movie_model.dart
import '../../domain/entities/movie_entity.dart';
class MovieModel extends MovieEntity {
const MovieModel(
{required String movieId,
required String title,
required String thumbnail,
required String movieUrl,
required bool unlocked})
: super(
movieId: movieId,
title: title,
thumbnail: thumbnail,
movieUrl: movieUrl,
unlocked: unlocked);
Map<String, dynamic> toJson() {
return <String, dynamic>{
'movieId': movieId,
'title': title,
'thumbnail': thumbnail,
'movieUrl': movieUrl,
'unlocked': unlocked,
};
}
factory MovieModel.fromJson(Map<String, dynamic> json) {
return MovieModel(
movieId: json['description'] as String,
title: json['title'] as String,
thumbnail: json['thumb'] as String,
movieUrl: json['sources'][0] as String,
unlocked: json['unlocked'] as bool,
);
}
}
In the model, you might see the super being used. This super is responsible to change the model into an entity (super passes the parameter to its parent constructor).
DataSources
Just like the name, they provide the data. They are the sources of data. There can be multiple data sources. For now, let’s go with a remote data source and create an interface for it.
Eg. movie data source. lib/features/movie/data/datasources/video_list_remote_datasource.dart
abstract class MovieListRemoteDataSource {
/// Throws a [ServerException] for all error codes.
Future<List<MovieModel>> getMovieList();
}
Now, let’s write a test.
Eg. Video List Remote DataSource Test. test/features/movie/data/datasources/video_list_datasource_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_show/core/error/exceptions.dart';
import 'package:movie_show/features/movie/data/datasources/video_list_remote_datasource.dart';
import 'package:movie_show/features/movie/data/models/movie_model.dart';
import 'package:movie_show/fixtures/fixture_reader.dart';
import '../../../../fixtures/fixture_reader.dart';
class MockMovie extends Mock implements MovieListRemoteDataSourceImpl {}
class MockProvideFakeData extends Mock implements ProvideFakeData {}
void main() {
late MovieListRemoteDataSourceImpl movieListRemoteDataSourceImpl;
late MockProvideFakeData mockProvideFakeData;
setUp(() {
mockProvideFakeData = MockProvideFakeData();
movieListRemoteDataSourceImpl =
MovieListRemoteDataSourceImpl(provideFakeData: mockProvideFakeData);
});
group('remote Data Source', () {
test('should return list of movie model', () async {
//Arrange
when(() => mockProvideFakeData.fixture('video_list.json'))
.thenAnswer((_) async => await fixture('video_list.json'));
//Act
final result = await movieListRemoteDataSourceImpl.getMovieList();
//Assert
verify(() => mockProvideFakeData.fixture('video_list.json'));
expect(result, isA<List<MovieModel>>());
});
test('should return serverexception if caught', () async {
//Arrange
when(() => mockProvideFakeData.fixture('video_list.json'))
.thenThrow(ServerException());
//Act
final result = movieListRemoteDataSourceImpl.getMovieList;
//Assert
expect(() async => await result.call(),
throwsA(const TypeMatcher<ServerException>()));
});
});
}
Where did the provide fake data come from? Remember data sources interact with the outside world, we are simulating the outside world with that class. When the function is supposed to throw an error, we should use thenthrow() and call a function throwsA() from the expect() function.
Eg. ProvideFake Data. lib/fixtures/fixture_reader.dart
import 'package:flutter/services.dart';
class ProvideFakeData {
Future<String> fixture(String name) async {
// for a file
return await rootBundle.loadString("assets/$name");
}
}
Now let’s write the implementation of the data source so that the test can pass.
Eg. movie data source. lib/features/movie/data/datasources/video_list_remote_datasource.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import '../../../../core/error/exceptions.dart';
import '../../../../fixtures/fixture_reader.dart';
import '../models/movie_model.dart';
abstract class MovieListRemoteDataSource {
/// Throws a [ServerException] for all error codes.
Future<List<MovieModel>> getMovieList();
}
class MovieListRemoteDataSourceImpl implements MovieListRemoteDataSource {
final ProvideFakeData provideFakeData;
MovieListRemoteDataSourceImpl({required this.provideFakeData});
[@override](http://twitter.com/override)
Future<List<MovieModel>> getMovieList() async {
List<MovieModel> movieList = [];
try {
final jsonMap =
json.decode(await provideFakeData.fixture('video_list.json'));
final result = jsonMap['categories'][0]['videos'];
for (var item in result) {
movieList.add(MovieModel.fromJson(item));
}
return movieList;
} catch (e) {
throw ServerException();
}
}
}
After this, the test will pass.
Repositories
It is the brain of the model deciding what to do with the data obtained from the data sources. It is the implementation of the repository defined in the domain layer.
Before writing the test we create an abstraction for the dependencies that depend on the NetworkInfo.
Eg . Network Info. lib/core/network/network_info.dart
import 'package:connectivity/connectivity.dart';
abstract class NetworkInfo {
Future<bool> get isConnected;
}
We don’t need to create the implementation of NetworkInfo as we are going to mock it (You can write implementation if you want but it won’t be used in the test). Now let’s write a test.
Eg . Repository Implementation test. test/features/movie/data/repositories/movie_repository_impl_test.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_show/core/error/exceptions.dart';
import 'package:movie_show/core/error/failure.dart';
import 'package:movie_show/core/network/network_info.dart';
import 'package:movie_show/features/movie/data/datasources/video_list_remote_datasource.dart';
import 'package:movie_show/features/movie/data/models/movie_model.dart';
import 'package:movie_show/features/movie/data/repositories/movie_repository_impl.dart';
import 'package:movie_show/features/movie/domain/entities/movie_entity.dart';
class MockRemoteDataSource extends Mock implements MovieListRemoteDataSource {}
class MockNetworkInfo extends Mock implements NetworkInfo {}
void main() {
late MovieRepositoryImpl movieRepositoryImpl;
late MockRemoteDataSource mockRemoteDataSource;
late MockNetworkInfo mockNetworkInfo;
final tMovieModelList = [
const MovieModel(
movieId: 'movieId',
title: 'title',
thumbnail: 'thumbnail',
movieUrl: 'movieUrl',
unlocked: true,
),
const MovieModel(
movieId: 'moviesIds',
title: 'titles',
thumbnail: 'thumbnails',
movieUrl: 'movieUrls',
unlocked: false,
)
];
final List<MovieEntity> tMovieEntityList = tMovieModelList;
setUp(() {
mockRemoteDataSource = MockRemoteDataSource();
mockNetworkInfo = MockNetworkInfo();
movieRepositoryImpl = MovieRepositoryImpl(
networkInfo: mockNetworkInfo,
movieListRemoteDataSource: mockRemoteDataSource,
);
});
group('getMovieList:', () {
test('should check if device is online', () async {
//Arrange
when(
() => mockNetworkInfo.isConnected,
).thenAnswer((_) async => true);
await mockNetworkInfo.isConnected;
//Assert
verify(() => mockNetworkInfo.isConnected);
});
group('when device is online', () {
setUp(() {
when(
() => mockNetworkInfo.isConnected,
).thenAnswer((_) async => true);
});
test('should return remote data when call is succesfull', () async {
//Arrange
when(
() => mockRemoteDataSource.getMovieList(),
).thenAnswer((_) async => tMovieModelList);
//Act
final result = await movieRepositoryImpl.getMovieList();
//Assert
verify(
() => mockRemoteDataSource.getMovieList(),
);
expect(result, equals(Right(tMovieEntityList)));
});
test('should return failure when call is unsuccesfull', () async {
//Arrange
when(
() => mockRemoteDataSource.getMovieList(),
).thenThrow(ServerException());
//Act
final result = await movieRepositoryImpl.getMovieList();
//Assert
verify(
() => mockRemoteDataSource.getMovieList(),
);
expect(result, equals(Left(ServerFailure())));
});
});
group('device is offline', () {
setUp(() {
when(
() => mockNetworkInfo.isConnected,
).thenAnswer((_) async => false);
});
test('should return failure', () async {
//Arrange
/// No arrange as datasource is not called if no network is found
//Act
final result = await movieRepositoryImpl.getMovieList();
//Assert
verifyNever(
() => mockRemoteDataSource.getMovieList(),
);
expect(result, equals(Left(ServerFailure())));
});
});
});
}
While checking for exceptions, we should not call the function as it results in exceptions. It should be called within the expect() function. While writing tests we should try to consider every possible scenario. The verifyNever verifies that mockRemoteDataSource.getMovieList() isn’t called.
Now let’s write the implementation of the repository
Eg. Movie Repository Implementation. lib/features/movie/data/repositories/movie_repository_impl.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/exceptions.dart';
import '../../../../core/error/failure.dart';
import '../../../../core/network/network_info.dart';
import '../../domain/entities/movie_entity.dart';
import '../../domain/repositories/movie_repository.dart';
import '../datasources/video_list_remote_datasource.dart';
class MovieRepositoryImpl implements MovieRepository {
final NetworkInfo networkInfo;
final MovieListRemoteDataSource movieListRemoteDataSource;
MovieRepositoryImpl({
required this.networkInfo,
required this.movieListRemoteDataSource,
});
[@override](http://twitter.com/override)
Future<Either<Failure, List<MovieEntity>>> getMovieList() async {
if (await networkInfo.isConnected) {
try {
final movieList = await movieListRemoteDataSource.getMovieList();
return Right(movieList);
} on ServerException {
return Left(ServerFailure());
}
} else {
return Left(ServerFailure());
}
}
}
After this, the test should pass and the data layer has been completed.
Finally, we have the following file and folder
DataLayer in lib |
DataLayer in test |
Fixture in test |
Lib structure |
In the next part, we will finish the presentation layer and dependency injection. See you there :)
Top comments (0)