DEV Community

Cover image for Learning TDD with Clean Architecture for Flutter (Data Layer) Part III
Safal Shrestha for CodingMountain

Posted on • Updated on

Learning TDD with Clean Architecture for Flutter (Data Layer) Part III

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.

data layer

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);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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()); 
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>()));
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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())));
      });
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
DataLayer in lib
Data Layer in test
DataLayer in test
Fixture in test
Fixture in test
Lib structure
Lib structure

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

Top comments (0)