MissingStubError: 'get' when using mockito 5.1.0 to test api using riverpod

MissingStubError: 'get' when using mockito 5.1.0 to test api using riverpod

我正在尝试使用 mockito 在 http.Client 调用中 return 伪造响应并能够测试该服务。我已经按照文档进行操作。它告诉我应该使用 annotate 来生成一个假的 class,但似乎是 flutter 的 null safe 导致了问题。有谁知道怎么做?修复它谢谢

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());

错误详情

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 文档: mockito unit testing

手动模拟 http.Client 很棘手。存根必须 完全 匹配参数。在你的例子中,你创建了一个存根:

when(mockHttp.get(url)).thenAnswer(...);

但错误表明实际调用的是什么:

No stub was found which matches the arguments of this method call:

get(<Long URL omitted>, {headers: {Content-Type: application/json}}\)

您的存根未注册提供 headers 参数的调用。

您真的应该避免尝试为 http.Client 创建手动模拟,而是使用 package:http 明确提供的 MockClient class。使用起来更方便。

确实,正如我的同事@jamesdlin 评论的那样,解决方案是使用 MockClient class,下面我分享了正确运行的实现,以防有人遇到这个问题,非常感谢 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>>());
  });
}