Skip to content

Commit

Permalink
repo impl test
Browse files Browse the repository at this point in the history
  • Loading branch information
mo7amedaliEbaid committed Oct 3, 2023
1 parent 1ccadf0 commit c73c95a
Show file tree
Hide file tree
Showing 14 changed files with 467 additions and 67 deletions.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

A new Flutter project.

## Getting Started
### Unit Tessting
How can you ensure that your app continues to work as you add more features or change existing functionality? By writing tests.

This project is a starting point for a Flutter application.
Unit tests are handy for verifying the behavior of a single function, method, or class. The test package provides the core framework for writing unit tests, and the flutter_test package provides additional utilities for testing widgets.
https://docs.flutter.dev/cookbook/testing/unit/introduction

A few resources to get you started if this is your first Flutter project:
### Mock dependencies using Mockito

- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
Sometimes, unit tests might depend on classes that fetch data from live web services or databases. This is inconvenient for a few reasons:

For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Calling live services or databases slows down test execution.
A passing test might start failing if a web service or database returns unexpected results. This is known as a “flaky test.”
It is difficult to test all possible success and failure scenarios by using a live web service or database.
Therefore, rather than relying on a live web service or database, you can “mock” these dependencies. Mocks allow emulating a live web service or database and return specific results depending on the situation.
https://docs.flutter.dev/cookbook/testing/unit/mocking
11 changes: 7 additions & 4 deletions lib/core/consts/consts.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'package:weather_tdd/apikey.dart';

sealed class Urls {
static const String baseUrl = 'https://api.openweathermap.org/data/2.5';
static const String apiKey = MYAPIKEY;
sealed class URLS {
static const String BASEURL = 'https://api.openweathermap.org/data/2.5';

static const String APIKEY = MYAPIKEY;

static String currentWeatherByName(String city) =>
'$baseUrl/weather?q=$city&appid=$apiKey';
'$BASEURL/weather?q=$city&appid=$APIKEY';

static String weatherIcon(String iconCode) =>
'http://openweathermap.org/img/wn/$iconCode@2x.png';
}
2 changes: 1 addition & 1 deletion lib/core/error/exception.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sealed class ServerException implements Exception {}
interface class ServerException implements Exception {}
4 changes: 2 additions & 2 deletions lib/core/error/failure.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ abstract class Failure extends Equatable {
List<Object> get props => [message];
}

sealed class ServerFailure extends Failure {
interface class ServerFailure extends Failure {
const ServerFailure(String message) : super(message);
}

sealed class ConnectionFailure extends Failure {
interface class ConnectionFailure extends Failure {
const ConnectionFailure(String message) : super(message);
}

Expand Down
31 changes: 31 additions & 0 deletions lib/data/data_sources/remote_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@


import 'dart:convert';

import '../../core/consts/consts.dart';
import '../../core/error/exception.dart';
import '../models/weather_model.dart';
import 'package:http/http.dart' as http;

abstract class WeatherRemoteDataSource {

Future<WeatherModel> getWeather(String cityName);
}


interface class WeatherRemoteDataSourceImpl extends WeatherRemoteDataSource {
final http.Client client;
WeatherRemoteDataSourceImpl({required this.client});

@override
Future < WeatherModel > getWeather(String cityName) async {
final response =
await client.get(Uri.parse(URLS.currentWeatherByName(cityName)));

if (response.statusCode == 200) {
return WeatherModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
}
28 changes: 28 additions & 0 deletions lib/data/repos/weather_repo_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'dart:io';

import 'package:dartz/dartz.dart';

import '../../core/error/exception.dart';
import '../../core/error/failure.dart';
import '../../domain/entities/weather.dart';
import '../../domain/repos/weather_repo.dart';
import '../data_sources/remote_data_source.dart';

interface class WeatherRepoImpl extends WeatherRepo {

final WeatherRemoteDataSource weatherRemoteDataSource;

WeatherRepoImpl({required this.weatherRemoteDataSource});

@override
Future < Either < Failure, WeatherEntity >> getWeather(String cityName) async {
try {
final result = await weatherRemoteDataSource.getWeather(cityName);
return Right(result.toEntity());
} on ServerException {
return const Left(ServerFailure('A server error has occurred, server error'));
} on SocketException {
return const Left(ConnectionFailure('Failed to connect to the network, connection error'));
}
}
}
37 changes: 37 additions & 0 deletions lib/presentation/bloc/weather_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:rxdart/rxdart.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import '../../domain/entities/weather.dart';
import '../../domain/usecases/get_weather.dart';
part 'weather_event.dart';

part 'weather_state.dart';

class WeatherBloc extends Bloc<WeatherEvent,WeatherState> {

final GetWeatherUseCase _getWeatherUseCase;
WeatherBloc(this._getWeatherUseCase) : super(WeatherEmpty()) {
on<OnCityChangedEvent>(
(event, emit) async {

emit(WeatherLoading());
final result = await _getWeatherUseCase.execute(event.cityName);
result.fold(
(failure) {
emit(WeatherFailure(failure.message));
},
(data) {
emit(WeatherLoaded(data));
},
);
},
transformer: debounce(const Duration(milliseconds: 500)),
);
}
}

EventTransformer<T> debounce<T>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
19 changes: 19 additions & 0 deletions lib/presentation/bloc/weather_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
part of 'weather_bloc.dart';


@immutable
abstract class WeatherEvent extends Equatable {
const WeatherEvent();

@override
List<Object?> get props => [];
}

class OnCityChangedEvent extends WeatherEvent {
final String cityName;

const OnCityChangedEvent(this.cityName);

@override
List<Object?> get props => [cityName];
}
32 changes: 32 additions & 0 deletions lib/presentation/bloc/weather_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
part of 'weather_bloc.dart';


@immutable
abstract class WeatherState extends Equatable {
const WeatherState();

@override
List<Object?> get props => [];
}

class WeatherEmpty extends WeatherState {}

class WeatherLoading extends WeatherState {}

class WeatherLoaded extends WeatherState {
final WeatherEntity result;

const WeatherLoaded(this.result);

@override
List<Object?> get props => [result];
}

class WeatherFailure extends WeatherState {
final String message;

const WeatherFailure(this.message);

@override
List<Object?> get props => [message];
}
58 changes: 58 additions & 0 deletions test/data/data_sources/remote_data_source_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
import 'package:weather_tdd/core/consts/consts.dart';
import 'package:weather_tdd/core/error/exception.dart';
import 'package:weather_tdd/data/data_sources/remote_data_source.dart';
import 'package:weather_tdd/data/models/weather_model.dart';

import '../../helpers/json_reader.dart';
import '../../helpers/test_helper.mocks.dart';

void main() {
late MockHttpClient mockHttpClient;
late WeatherRemoteDataSourceImpl weatherRemoteDataSourceImpl;

setUp(() {
mockHttpClient = MockHttpClient();
weatherRemoteDataSourceImpl =
WeatherRemoteDataSourceImpl(client: mockHttpClient);
});

const testCityName = 'Cairo';

group('get weather testing group', () {
test(
'should return weather model when the response code is 200, It is a successful test',
() async {
//arrange
when(mockHttpClient
.get(Uri.parse(URLS.currentWeatherByName(testCityName))))
.thenAnswer((_) async => http.Response(
readJson('helpers/dummy_data/dummy_weather_response.json'), 200));

//act
final result = await weatherRemoteDataSourceImpl.getWeather(testCityName);

//assert
expect(result, isA<WeatherModel>());
});

test(
'should throw a server exception when the response code is 404 or other, It is a successful test',
() async {
//arrange
when(
mockHttpClient
.get(Uri.parse(URLS.currentWeatherByName(testCityName))),
).thenAnswer((_) async => http.Response('Not found', 404));

//act
final result = weatherRemoteDataSourceImpl.getWeather(testCityName);

//assert
expect(result, throwsA(isA<ServerException>()));
},
);
});
}
99 changes: 99 additions & 0 deletions test/data/repos/weather_repo_impl_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import 'dart:io';

import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:weather_tdd/core/error/exception.dart';
import 'package:weather_tdd/core/error/failure.dart';
import 'package:weather_tdd/data/models/weather_model.dart';
import 'package:weather_tdd/data/repos/weather_repo_impl.dart';
import 'package:weather_tdd/domain/entities/weather.dart';

import '../../helpers/test_helper.mocks.dart';

void main() {

late MockWeatherRemoteDataSource mockWeatherRemoteDataSource;
late WeatherRepoImpl weatherRepoImpl;

setUp(() {
mockWeatherRemoteDataSource = MockWeatherRemoteDataSource();
weatherRepoImpl = WeatherRepoImpl(
weatherRemoteDataSource: mockWeatherRemoteDataSource,
);
});

const testWeatherModel = WeatherModel(
cityName: 'Cairo',
main: 'Clouds',
description: 'few clouds',
iconCode: '02d',
temperature: 304.28,
pressure: 1007,
humidity: 72,
);

const testWeatherEntity = WeatherEntity(
cityName: 'Cairo',
main: 'Clouds',
description: 'few clouds',
iconCode: '02d',
temperature: 304.28,
pressure: 1007,
humidity: 72,
);

const testCityName = 'Cairo';


group('get weather testing group, reop impl test', () {

test(
'should return weather when a call to data source is successful, It is a successful test',
() async {
// arrange
when(mockWeatherRemoteDataSource.getWeather(testCityName))
.thenAnswer((_) async => testWeatherModel);

// act
final result = await weatherRepoImpl.getWeather(testCityName);

// assert
expect(result, equals(const Right(testWeatherEntity)));
},
);

test(
'should return server failure when a call to data source is unsuccessful',
() async {
// arrange
when(mockWeatherRemoteDataSource.getWeather(testCityName))
.thenThrow(ServerException());

// act
final result = await weatherRepoImpl.getWeather(testCityName);

// assert
expect(result, equals(const Left(ServerFailure('A server error has occurred, server error'))));
},
);

test(
'should return connection failure when the device has no internet, connection error, this is a successful test',
() async {
// arrange
when(mockWeatherRemoteDataSource.getWeather(testCityName))
.thenThrow(const SocketException('Failed to connect to the network, socket error'));

// act
final result = await weatherRepoImpl.getWeather(testCityName);

// assert
expect(result, equals(const Left(ConnectionFailure('Failed to connect to the network, connection error'))));
},
);

});


}
6 changes: 5 additions & 1 deletion test/helpers/test_helper.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import 'package:mockito/annotations.dart';
import 'package:http/http.dart' as http;
import 'package:weather_tdd/data/data_sources/remote_data_source.dart';
import 'package:weather_tdd/domain/repos/weather_repo.dart';
import 'package:weather_tdd/domain/usecases/get_weather.dart';

@GenerateMocks(
[
WeatherRepo
WeatherRepo,
WeatherRemoteDataSource,
GetWeatherUseCase
],
customMocks: [MockSpec<http.Client>(as: #MockHttpClient)],
)
Expand Down
Loading

0 comments on commit c73c95a

Please sign in to comment.