Learning TDD with Clean Architecture for Flutter (Data Layer) Part III
Tags: flutter, cleanarchitecture, dart, tdd
Published: 2022-09-22 04:57:09



Part I [here](https://dev.to/codingmountain/learning-tdd-with-clean-architecture-for-flutter-part-i-46nk)

Part II [here](https://dev.to/codingmountain/learning-tdd-with-clean-architecture-for-flutter-domain-layer-part-ii-1pi5)

Part **IV [here](https://dev.to/codingmountain/learning-tdd-with-clean-architecture-for-flutter-part-iv-16b4)**

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.

![data layer](https://cdn-images-1.medium.com/max/2000/1*YvVW937gxOeSNzzawbTeeA.png)

### 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](https://gist.github.com/jsturgis/3b19447b304616f18657). 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](https://gist.github.com/jsturgis/3b19447b304616f18657)

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

| ![Data Layer in lib](https://cdn-images-1.medium.com/max/2000/1*J7HWfL0Vhnyd8UHpK3WKLw.png)|
|:--:|
| <b>DataLayer in lib</b>|



| ![Data Layer in test](https://cdn-images-1.medium.com/max/2000/1*bh5P57tMSNVXk69OSF0aAA.png)|
|:--:|
| <b>DataLayer in test</b>|

| ![Fixture in test](https://cdn-images-1.medium.com/max/2000/1*GzJ7V9sdgMNKApdguG-8WA.png)|
|:--:|
| <b>Fixture in test</b>|

| ![Lib structure](https://miro.medium.com/max/560/1*fuGfw-dNIdnD4ymQVEavdQ.png)|
|:--:|
| <b>Lib structure</b>|


In the next part, we will finish the presentation layer and dependency injection. See you there :)