Search code examples
flutterdartmockingtypeerror

Flutter - type 'Null' is not a subtype of type 'Future<Response<dynamic>>' when mocking Dio's get method


I am trying to mock Dio's get method. The mock is working fine as per my test. However, when calling inside the test type 'Null' is not a subtype of type 'Future<Response<dynamic>>'.

I have called newsApi.get('/top-headlines') during test as well. And, I can assure it that the mock is returning data fine. But for unknown reasons, the call inside NewsService is giving null. Could you please guide me in solving the issue?

Filename: services/news.dart

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:newsapp/enums/news_category.dart';
import 'package:newsapp/enums/news_country.dart';
import 'package:newsapp/models/articles.dart';
import 'package:newsapp/models/error.dart';

import '../main.dart';

class NewsService {
  final Dio newsApi;

  NewsService({required this.newsApi});

  Future<dynamic> getArticlesByCategory(
    NewsCategory category, {
    int page = 1,
    int pageSize = 100,
    NewsCountry country = NewsCountry.US,
  }) async {
    final response = await newsApi.get('top-headlines', queryParameters: {
      'category': category.name,
      'country': country.name.toLowerCase(),
      'page': page,
      'pageSize': pageSize,
    });

    print(response);

    await newsApi.get('top-headlines', queryParameters: {
      'category': category.name,
      'country': country.name.toLowerCase(),
      'page': page,
      'pageSize': pageSize,
    }).then((response) {
      if (response.statusCode == HttpStatus.ok) {
        if (response.data['status'] == 'ok') {
          return Articles.fromJson(response.data);
        } else {
          return Error.fromJson(response.data);
        }
      } else if (response.statusCode == HttpStatus.unauthorized) {
        return Error.fromJson(response.data);
      } else {
        return Future.error(
            'Failure processing request. Please try again later.');
      }
    }, onError: (error) {
      print(error);
      logger.e(error);
      return Future.error(error);
    }).catchError((error) {
      print(error);
      logger.e(error);
      return error;
    });
  }
}

Filename: test/news.dart

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:newsapp/enums/news_category.dart';
import 'package:newsapp/models/articles.dart';
import 'package:newsapp/services/news.dart';

import '../mocks/dio.dart';

void main() async {
  group('NewsService tests', () {
    //Arrange
    late MockDio newsApi;

    group('NewsService.getArticles() tests', () {
      setUp(() {
        newsApi = MockDio();

        Future<Response> responseMethod = Future.value(Response(
            data: {
              "status": "ok",
              "totalResults": 11207,
              "articles": [
                {
                  "source": {"id": "bbc-news", "name": "BBC News"},
                  "author": "https://www.facebook.com/bbcnews",
                  "title": "Indian PM Modi's Twitter hacked with bitcoin tweet",
                  "description":
                      "The Indian prime minister's account had a message stating that bitcoin would be distributed to citizens.",
                  "url": "https://www.bbc.co.uk/news/world-asia-india-59627124",
                  "urlToImage":
                      "https://ichef.bbci.co.uk/news/1024/branded_news/5998/production/_122063922_mediaitem122063921.jpg",
                  "publishedAt": "2021-12-12T10:59:57Z",
                  "content":
                      "Image source, AFP via Getty Images\r\nImage caption, Modi has has more than 70 million Twitter followers\r\nIndian Prime Minister Narendra Modi's Twitter account was hacked with a message saying India ha… [+854 chars]"
                },
                {
                  "source": {"id": null, "name": "New York Times"},
                  "author": "Corey Kilgannon",
                  "title": "Why New York State Is Experiencing a Bitcoin Boom",
                  "description":
                      "Cryptocurrency miners are flocking to New York’s faded industrial towns, prompting concern over the environmental impact of huge computer farms.",
                  "url":
                      "https://www.nytimes.com/2021/12/05/nyregion/bitcoin-mining-upstate-new-york.html",
                  "urlToImage":
                      "https://static01.nyt.com/images/2021/11/25/nyregion/00nybitcoin5/00nybitcoin5-facebookJumbo.jpg",
                  "publishedAt": "2021-12-06T00:42:28Z",
                  "content":
                      "The plant opening northeast of Niagara Falls this month, in Somerset, N.Y., is part of a \$550 million project by Terawulf, a Bitcoin mining company. The project also includes a proposed 150-megawatt … [+1514 chars]"
                }
              ]
            },
            statusCode: HttpStatus.ok,
            requestOptions: RequestOptions(path: '/top-headlines')));

        when(() => newsApi.get(
              '/top-headlines',
              queryParameters: any(named: 'queryParameters'),
              options: any(named: 'options'),
              cancelToken: any(named: 'cancelToken'),
              onReceiveProgress: any(named: 'onReceiveProgress'),
            )).thenAnswer((_) => responseMethod);
      });

      tearDown(() {
        reset(newsApi);
      });

      test('Get Articles', () async {
        // Arrange
        NewsService newsService = NewsService(newsApi: newsApi);

        final response = await newsApi.get('/top-headlines');
        print('Response');
        print(response.data);

        final articles = Articles.fromJson(response.data);

        print(articles);

        // Act
        await newsService.getArticlesByCategory(NewsCategory.business);

        // Assert
        verify(() => newsApi.get('/top-headlines',
            queryParameters: any(named: 'queryParameters'))).called(1);
      });
    });
  });
}


Solution

  • Finally I've got a working solution.

    It seems that Dio needs a seperate mock library to mock itself using adapters. The library to use is http_mock_adapter: ^0.1.4 and its below is its link.

    LINK: Http Mock Library

    import 'dart:convert';
    
    import 'package:dio/dio.dart';
    import 'package:http_mock_adapter/http_mock_adapter.dart';
    import 'package:test/test.dart';
    
    void main() async {
      // How to mock with DioAdapter
      group('DioAdapter usage', () {
        // Creating dio instance for mocking.
        // For instance: you can use your own instance from injection and replace
        // dio.httpClientAdapter with mocker DioAdapter
    
        const path = 'https://example.com';
    
        test('Expects Dioadapter to mock the data', () async {
          final dio = Dio();
          final dioAdapter = DioAdapter();
    
          dio.httpClientAdapter = dioAdapter;
          dioAdapter
              .onGet(path)
              .reply(200,
                  {'message': 'Successfully mocked GET!'}) // only use double quotes
              .onPost(path)
              .reply(200, {'message': 'Successfully mocked POST!'});
    
          // Making dio.get request on the path an expecting mocked response
          final getResponse = await dio.get(path);
          expect(jsonEncode({'message': 'Successfully mocked GET!'}),
              getResponse.data);
    
          // Making dio.post request on the path an expecting mocked response
          final postResponse = await dio.post(path);
          expect(jsonEncode({'message': 'Successfully mocked POST!'}),
              postResponse.data);
        });
    
        // Alternatively you can use onRoute chain to pass custom requests
        test('Expects Dioadapter to mock the data with onRoute', () async {
          final dio = Dio();
          final dioAdapter = DioAdapter();
    
          dio.httpClientAdapter = dioAdapter;
          dioAdapter
              .onRoute(path, request: Request(method: RequestMethods.PATCH))
              .reply(200, {
                'message': 'Successfully mocked PATCH!'
              }) // only use double quotes
              .onRoute(path, request: Request(method: RequestMethods.DELETE))
              .reply(200, {'message': 'Successfully mocked DELETE!'});
    
          // Making dio.get request on the path an expecting mocked response
          final patchResponse = await dio.patch(path);
          expect(jsonEncode({'message': 'Successfully mocked PATCH!'}),
              patchResponse.data);
    
          // Making dio.post request on the path an expecting mocked response
          final deleteResposne = await dio.delete(path);
          expect(jsonEncode({'message': 'Successfully mocked DELETE!'}),
              deleteResposne.data);
        });
      });
    
      // Also, for mocking requests, you can use dio Interceptor
      group('DioInterceptor usage', () {
        // Creating dio instance for mocking.
        // For instance: you can use your own instance from injection and add
        // DioInterceptor in dio.interceptors list
        final dioForInterceptor = Dio();
        final dioInterceptor =
            DioInterceptor(); // creating DioInterceptor instance for mocking requests
    
        dioForInterceptor.interceptors.add(dioInterceptor);
    
        const path = 'https://example2.com';
    
        test('Expects Dioadapter to mock the data', () async {
          // Defining request types and their responses respectively with their paths
          dioInterceptor
              .onDelete(path)
              .reply(200,
                  {'message': 'Successfully mocked GET!'}) // only use double quotes
              .onPatch(path)
              .reply(200, {'message': 'Successfully mocked POST!'});
    
          // Making dio.delete request on the path an expecting mocked response
          final getResponse = await dioForInterceptor.delete(path);
          expect(jsonEncode({'message': 'Successfully mocked GET!'}),
              getResponse.data);
    
          // Making dio.patch request on the path an expecting mocked response
          final postResposne = await dioForInterceptor.patch(path);
          expect(jsonEncode({'message': 'Successfully mocked POST!'}),
              postResposne.data);
        });
      });
    
      group('Raising the custrom Error onRequest', () {
        const path = 'https://example.com';
    
        test('Test that throws raises custom exception', () async {
          final dio = Dio();
          final dioAdapter = DioAdapter();
    
          dio.httpClientAdapter = dioAdapter;
          const type = DioErrorType.RESPONSE;
          final response = Response(statusCode: 500);
          const error = 'Some beautiful error';
    
          // Building request to throw the DioError exception
          // on onGet for the specific path
          dioAdapter.onGet(path).throws(
                500,
                DioError(
                  type: type,
                  response: response,
                  error: error,
                ),
              );
    
          // Checking that exception type can match `AdapterError` type too
          expect(() async => await dio.get(path),
              throwsA(TypeMatcher<AdapterError>()));
    
          // Checking that exception type can match `DioError` type too
          expect(() async => await dio.get(path), throwsA(TypeMatcher<DioError>()));
    
          // Checking the type and the message of the exception
          expect(
              () async => await dio.get(path),
              throwsA(
                  predicate((DioError e) => e is DioError && e.message == error)));
        });
      });
    }