I'm learing test writing with mockito but i have some problems with testing remote_data_source_class.
Here's my abstract class
abstract class ApiRemoteDataSource {
Future<PokemonsListResponseModel> getPokemons();
}
Here's the implementation
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:http/http.dart' as http;
import '../../models/pokemons_list_response_model.dart';
import 'api_remote_data_source.dart';
#Injectable(as: ApiRemoteDataSource)
class ApiRemoteDataSourceImpl implements ApiRemoteDataSource {
ApiRemoteDataSourceImpl(this.client);
final http.Client client;
final pokemonListUrl = Uri.parse('https://pokeapi.co/api/v2/pokemon');
#override
Future<PokemonsListResponseModel> getPokemons() async {
final response = await client.get(pokemonListUrl);
final data = response.body;
final modelData = PokemonsListResponseModel.fromJson(json.decode(data));
return modelData;
}
}
Now Iwant to verify that when i invoke method .getPokemons() on mock data source my http.Client will execute a call to given endpoint:
#GenerateMocks([http.Client, ApiRemoteDataSource])
void main() {
late ApiRemoteDataSource dataSource;
late MockClient mockHttpClient;
setUp(() async {
await configureDependencies();
mockHttpClient = MockClient();
dataSource = MockApiRemoteDataSource();
print('test: $mockHttpClient, data source $dataSource');
});
group('getPokemonsList', () {
test('Should perform a GET request on a URL', () async {
final url = Uri.parse('https://pokeapi.co/api/v2/pokemon');
// arrange
when(mockHttpClient.get(url, headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response(fixture('pokemon_list.json'), 200));
// act
dataSource.getPokemons();
// assert
verify(mockHttpClient.get(Uri.parse('https://pokeapi.co/api/v2/pokemon')));
});
});
}
Running test above gives me this error: "MissingStubError: 'getPokemons'
No stub was found which matches the arguments of this method call:
getPokemons()"
When i replace dataSource.getPokemons() in //act part with "mockHttpClient.get(url)" everything works but I'm not sure if that kind of test is valid
Related
This is my first try to write a test. I use the Dartz package and I don't know how to get just one part as a result of my test. The error is:
Expected: Right<dynamic, NumberTrivia>:<Right(NumberTrivia(test, 1))>
Actual: <Instance of 'Future<Either<Failure, NumberTrivia>>'>
The function in repository:
abstract class NumberTriviaRepository {
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia();
}
The function in usecase:
class GetConcreteNumberTrivia {
final NumberTriviaRepository numberTriviaRepository;
GetConcreteNumberTrivia(this.numberTriviaRepository);
Future<Either<Failure, NumberTrivia>> execute({
required int number,
}) async {
return await numberTriviaRepository.getConcreteNumberTrivia(number);
}
}
And the test class:
import 'package:dartz/dartz.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:number_trivia/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:number_trivia/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:number_trivia/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart';
import 'get_concrete_number_trivia_test.mocks.dart';
#GenerateMocks([NumberTriviaRepository])
void main() {
late GetConcreteNumberTrivia usecase;
late MockNumberTriviaRepository mockNumberTriviaRepository;
setUp(() {
mockNumberTriviaRepository = MockNumberTriviaRepository();
usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
});
final tNumber = 1;
final tNumberTrivia = NumberTrivia(text: 'test', number: tNumber);
test('should get trivia for the number from the repository', () async {
// arrange
when(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber))
.thenAnswer((_) async => Right(tNumberTrivia));
// act
final result = usecase.execute(number: tNumber);
// assets
expect(result, Right(tNumberTrivia));
verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
verifyNoMoreInteractions(mockNumberTriviaRepository);
});
}
The issue coming because usecase.execute(number: tNumber); retuning future, use await before it.
// act
final result = await usecase.execute(number: tNumber);
I am trying to use mockito to return a fake response in the http.Client call and be able to test the service. I have followed the documentation. It tells me that I should use annotate to generate a fake class, but it seems that the null safe of flutter is causing problems. Does anyone know how? fix it thanks
movies_provider_test.dart
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:watch_movie_app/src/data/data_source/remote/http_request.dart';
import 'package:watch_movie_app/src/data/models/models.dart';
import 'package:watch_movie_app/src/domain/services/movie_service.dart';
import 'package:watch_movie_app/src/environment_config.dart';
import 'mocks/popular_movies.dart';
import 'movies_provider_test.mocks.dart';
#GenerateMocks([http.Client])
void main() {
test('returns an movies if the http call completes sucessfully', () async {
final mockHttp = MockClient();
final container = ProviderContainer(
overrides: [
httpClientProvider.overrideWithValue(HttpRequest(httpClient: mockHttp)),
],
);
addTearDown(container.dispose);
final environmentConfig = container.read(environmentConfigProvider);
final movieService = container.read(movieServiceProvider);
String urlApi =
"${environmentConfig.domainApi}/${environmentConfig.apiVersion}/tv/popular?api_key=${environmentConfig.movieApiKey}&language=en-US&page=1";
Uri url = Uri.parse(urlApi);
when(mockHttp.get(url)).thenAnswer(
(_) async => http.Response(fakeMovies, 200),
);
expectLater(await movieService.getMovies(), isInstanceOf<List<Movie>>());
});
}
movie_service.dart
import 'package:http/http.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:watch_movie_app/src/data/data_source/remote/http_request.dart';
import 'package:watch_movie_app/src/domain/enums/enums.dart';
import 'package:watch_movie_app/src/data/models/models.dart';
import 'package:watch_movie_app/src/environment_config.dart';
import 'package:watch_movie_app/src/helpers/movie_api_exception.dart';
final movieServiceProvider = Provider<MovieService>((ref) {
final config = ref.read(environmentConfigProvider);
final httpRequest = ref.read(httpClientProvider);
return MovieService(config, httpRequest);
});
class MovieService {
final EnvironmentConfig _environmentConfig;
final HttpRequest _http;
MovieService(this._environmentConfig, this._http);
Future<List<Movie>> getMovies() async {
try {
String url =
"${_environmentConfig.domainApi}/${_environmentConfig.apiVersion}/tv/popular?api_key=${_environmentConfig.movieApiKey}&language=en-US&page=1";
final response =
await _http.request(typeHttp: EnumHttpType.get, urlApi: url);
if (response.statusCode != 200) {
throw const MovieApiException('Error al consulta las series populares');
}
List<Movie> movies = allMoviesFromJson(response.body);
return movies;
} on ClientException {
throw const MovieApiException('Error al consultar la informaciĆ³n');
}
}
Future<List<Movie>> getMoviesRecommendations() async {
try {
String url =
"${_environmentConfig.domainApi}/${_environmentConfig.apiVersion}/tv/top_rated?api_key=${_environmentConfig.movieApiKey}&language=en-US&page=1";
final response =
await _http.request(typeHttp: EnumHttpType.get, urlApi: url);
if (response.statusCode != 200) {
throw const MovieApiException(
'Error al consulta las series recomendadas');
}
List<Movie> movies = allMoviesFromJson(response.body);
return movies;
} on ClientException {
throw const MovieApiException('Error al consultar los recomendados');
}
}
Future<MovieExtend> getDetailMovie(int id) async {
try {
String url =
"${_environmentConfig.domainApi}/${_environmentConfig.apiVersion}/tv/$id?api_key=${_environmentConfig.movieApiKey}&language=en-US&page=1";
final Response response =
await _http.request(typeHttp: EnumHttpType.get, urlApi: url);
if (response.statusCode != 200) {
throw const MovieApiException(
'Error al consulta el detalle de la serie');
}
MovieExtend movieExtend = movieExtendFromJson(response.body);
return movieExtend;
} on ClientException {
throw const MovieApiException(
'Error al consultar el detalle de la serie');
}
}
Future<List<Movie>> getAirtodayMovies() async {
try {
String url =
"${_environmentConfig.domainApi}/${_environmentConfig.apiVersion}/tv/airing_today?api_key=${_environmentConfig.movieApiKey}&language=en-US&page=1";
final Response response =
await _http.request(typeHttp: EnumHttpType.get, urlApi: url);
if (response.statusCode != 200) {
throw const MovieApiException(
'Error al consultar las series, intente nuevamente mas tarde');
}
List<Movie> movies = allMoviesFromJson(response.body);
return movies;
} on ClientException {
throw const MovieApiException('Error al consultar las series de hoy');
}
}
}
htt_request.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:watch_movie_app/src/domain/enums/enums.dart';
/// Clase que nos permite hacer peticiones Http
/// usando la libreria http.dar
class HttpRequest {
final http.Client _httpClient;
late String? token;
HttpRequest({http.Client? httpClient})
: _httpClient = httpClient ?? http.Client();
Future<http.Response> request(
{required EnumHttpType typeHttp, required String urlApi, data}) async {
Map<String, String> headers = {'Content-Type': 'application/json'};
Uri url = Uri.parse(urlApi);
switch (typeHttp) {
case EnumHttpType.post:
return _httpClient.post(url, body: data, headers: headers);
case EnumHttpType.get:
return _httpClient.get(url, headers: headers);
case EnumHttpType.patch:
return _httpClient.patch(url, headers: headers);
case EnumHttpType.put:
return _httpClient.put(url, headers: headers);
case EnumHttpType.delete:
return _httpClient.delete(url, headers: headers);
default:
return _httpClient.get(url);
}
}
}
final httpClientProvider = Provider<HttpRequest>((ref) => HttpRequest());
error detail
MissingStubError: 'get'
No stub was found which matches the arguments of this method call:
get(https://api.themoviedb.org/3/tv/popular?api_key=4dc138c853e44e4ea1d3dfd746fe451d&language=en-US&page=1, {headers: {Content-Type: application/json}}\)
Add a stub for this method using Mockito's 'when' API, or generate the MockClient mock with a MockSpec with 'returnNullOnMissingStub: true' (see https://pub.dev/documentation/mockito/latest/annotations/MockSpec-class.html\).
package:mockito/src/mock.dart 191:7 Mock._noSuchMethod
package:mockito/src/mock.dart 185:45 Mock.noSuchMethod
test\movies_provider_test.mocks.dart 45:14 MockClient.get
package:watch_movie_app/src/data/data_source/remote/http_request.dart 23:28 HttpRequest.request
package:watch_movie_app/src/domain/services/movie_service.dart 26:23 MovieService.getMovies
test\movies_provider_test.dart 36:36 main.<fn>
test\movies_provider_test.dart 17:68
link doc:
mockito unit testing
Manually mocking http.Client is tricky. Stubs must match arguments exactly. In your case, you created a stub for:
when(mockHttp.get(url)).thenAnswer(...);
but the error indicates what was actually called:
No stub was found which matches the arguments of this method call:
get(<Long URL omitted>, {headers: {Content-Type: application/json}}\)
Your stub is not registered for a call that supplies a headers argument.
You really should avoid trying to create a manual mock for http.Client and instead use the MockClient class explicitly provided by package:http. It's much easier to use.
Indeed, as my colleague #jamesdlin commented, the solution was to use the MockClient class, below I share the implementation working correctly in case someone goes through this, thank you very much jamesdlin
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:watch_movie_app/src/data/data_source/remote/http_request.dart';
import 'package:watch_movie_app/src/data/models/models.dart';
import 'package:watch_movie_app/src/domain/services/movie_service.dart';
import 'mocks/popular_movies.dart';
void main() {
test('returns an instance of movies if the http call completed sucessfully',
() async {
final mockHttp = MockClient((_) async => http.Response(fakeMovies, 200));
final container = ProviderContainer(
overrides: [
httpClientProvider.overrideWithValue(HttpRequest(httpClient: mockHttp)),
],
);
addTearDown(container.dispose);
final movieService = container.read(movieServiceProvider);
expectLater(await movieService.getMovies(), isInstanceOf<List<Movie>>());
});
}
I've been spinning my wheels for hours on the simple question of how to inject http.Client into a flutter class when using injectable. They reference doing this in a module (as suggested in this post), but I can't figure that out either.
This is my file (abstract and concrete classes):
import 'dart:convert';
import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
import 'package:injectable/injectable.dart';
import 'package:myapp_flutter/core/errors/exceptions.dart';
import 'package:myapp_flutter/data/models/sample_model.dart';
abstract class ISampleRemoteDataSource {
/// Throws a [ServerException] for all error codes.
Future<SampleModel> getSampleModel(String activityType);
}
#Injectable(as: ISampleRemoteDataSource)
class SampleRemoteDataSourceImpl extends ISampleRemoteDataSource {
final http.Client client;
final baseUrl = "https://www.boredapi.com/api/activity?type=";
final headers = {'Content-Type': 'application/json'};
SampleRemoteDataSourceImpl({#factoryParam required this.client});
#override
Future<SampleModel> getSampleModel(String activityType) async {
Uri uri = Uri.parse(baseUrl + activityType);
GetIt.I.get<http.Client>();
final response = await client.get(uri, headers: headers);
if (response.statusCode == 200) {
return SampleModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
}
I thought declaring it as a factory param in the constructor would do it, but I was wrong. Declaring the abstract class as a module doesn't do it (and seems very wrong, also). I just don't know.
This should work:
First implement a register module file
#module
abstract class RegisterModule {
//add http client
#lazySingleton
http.Client get httpClient => http.Client();
}
class $RegisterModule extends RegisterModule {}
Then, after generating injection.config.dart file your http Client should be mentioned in initGetIt class
You will have to register them as module as described below
https://pub.dev/packages/injectable#Registering-third-party-types
I have written a test for a simple HTTP get using Mocktail to mock the HTTP client. When I call the get method in the test I receive "type 'Null' is not a subtype of type 'Future'".
Anyone any idea why this might be?
Here is the test:
class MockHttpClient extends Mock implements http.Client {}
void main() {
late IdRemoteDataSourceImpl dataSource;
late MockHttpClient mockHttpClient;
setUp(
() {
mockHttpClient = MockHttpClient();
dataSource = IdRemoteDataSourceImpl(client: mockHttpClient);
},
);
group('Get id', () {
test(
'when response code is 200',
() async {
final url = Uri.parse('https://api.test.com/');
const tUsername = 'username';
final accountJson = json.decode(
fixture('account.json'),
// ignore: avoid_as
) as Map<String, dynamic>;
final tIdModel = IdModel.fromJson(accountJson);
// arrange
when(() => mockHttpClient.get(url))
.thenAnswer((_) async => http.Response(
fixture('account.json'),
200,
));
// act
final testResult = await dataSource.getId(tUsername);
// assert
// expect(testResult, tIdModel);
},
);
});
}
The error occurs when the following line runs:
final testResult = await dataSource.getId(tUsername);
Code being tested:
import 'dart:convert';
import 'package:http/http.dart' as http;
class IdModel {
IdModel({required this.id});
final String id;
factory IdModel.fromJson(Map<String, dynamic> json) {
return IdModel(id: json['id'].toString());
}
}
abstract class IdRemoteDataSource {
Future<IdModel> getId(String username);
}
class IdRemoteDataSourceImpl implements IdRemoteDataSource {
IdRemoteDataSourceImpl({required this.client});
final http.Client client;
#override
Future<IdModel> getId(String username) async {
final url = Uri.parse('https://api.test.com/query?username=$username');
final response = await client.get(url);
// ignore: avoid_as
final responseJson = json.decode(response.body) as Map<String, dynamic>;
return IdModel.fromJson(responseJson);
}
}
Error type 'Null' is not a subtype of type 'Future'... occurs when you call method that has not been implemented for mock object or there are different parameters passed to it.
In your code you passed different url parameter to get(...) method. Http client mock waiting for 'https://api.test.com/' but actually 'https://api.test.com/query?username=$username' has been passed.
You have two options to solve it.
Pass the same url to mocked method from when(...) that will be passed during test:
const tUsername = 'username';
final url = Uri.parse('https://api.test.com/query?username=$tUsername');
...
// arrange
when(() => mockHttpClient.get(url))
.thenAnswer((_) async => http.Response(
fixture('account.json'),
200,
),
);
Use any matcher (if you don't care which parameter passed):
registerFallbackValue(Uri.parse(''));
...
when(() => mockHttpClient.get(any()))
.thenAnswer((_) async => http.Response(
fixture('account.json'),
200,
),
);
I try to get List from jsonPlaceHolder using flutter rxdart stream and try to apply bloc pattern on it.
this class that response for get post response from api
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/post_item.dart';
class ItemApi {
Future<List<JsonItem>> getPost() async {
String _url = 'https://jsonplaceholder.typicode.com/posts';
final _response = await http.get(_url);
if (_response.statusCode == 200) {
return (json.decode(_response.body) as List)
.map((jsonItem) => JsonItem.fromJson(jsonItem))
.toList();
}
}
}
I using repository class to wrap using ItemApi class
import 'json_item_request.dart';
import '../models/post_item.dart';
class Repository{
final jsonItemResponse = ItemApi();
Future<List<JsonItem>> getItem() => jsonItemResponse.getPost();
}
at the last i using bloc class that response for get data and set it inside PublishSubject
import '../models/post_item.dart';
import '../resouces/repository.dart';
import 'package:rxdart/rxdart.dart';
class JsonBloc {
final _repository = Repository();
final _streamOfJsonList = PublishSubject<List<JsonItem>>();
Observable<List<JsonItem>> get jsonList=> _streamOfJsonList.stream;
fetchAllPost() async{
Future<List<JsonItem>> list = _repository.getItem();
}
dispose(){
_streamOfJsonList.close();
}
}
My question is how i can set response inside _streamOfJsonList variable to using it when list changed.
Sounds like you already have all the moving parts connected? If so you just need to add the item list to the PublishSubject:
void fetchAllPost() async {
List<JsonItem> list = await _repository.getItem();
_streamOfJsonList.add(list);
}
This will trigger the onListen callback with the new list on anything that is listening to the stream.
You can add error and data to ReplaySubject like below :
void fetchAllPost() async {
List<JsonItem> list = await _repository.getItem();
if (list != null) {
_streamOfJsonList.sink.add(list);
} else {
_streamOfJsonList.addError("ERROR");
}
}