Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selective mocking #136

Closed
ekimia opened this issue Nov 4, 2022 · 11 comments
Closed

Selective mocking #136

ekimia opened this issue Nov 4, 2022 · 11 comments
Labels
enhancement New feature or request

Comments

@ekimia
Copy link

ekimia commented Nov 4, 2022

Description

During development, it's super useful to only mock certain endpoints - like new endpoints that don't exist yet. Right now from what I can tell it's either you mock every single endpoint or completely turn it off.

@cyberail
Copy link
Collaborator

cyberail commented Nov 4, 2022

You mean to have some real routes and some mocked ones together ?

@cyberail cyberail added enhancement New feature or request help wanted Extra attention is needed and removed help wanted Extra attention is needed labels Nov 4, 2022
@HeLinChooi
Copy link

I found a solution to do that.

We could create another Dio object for remote API callings.

@ekimia
Copy link
Author

ekimia commented Jan 25, 2023

@HeLinChooi thats the approach i did as well.

Specifically, I have an APIService class that has 2 instances of dio under the hood. passing useMock will use the mock version of dio and throw an error if the mock isnt found.

@sebastianbuechler
Copy link
Collaborator

You mean to have some real routes and some mocked ones together ?

In the Javascript world, there is the MSW package which lets you define if it should match all the routes or just the defined ones. It's a common usecase for development to only want to mock a new (non-existent) endpoint in order to already develop without having a working backend and let the other routes go to the existing back-end as usual.

Such a feature would increase the value of this package by a lot! Is it possible to add such a config parameter or would it require much more work?

@LukaGiorgadze
Copy link
Member

You mean to have some real routes and some mocked ones together ?

In the Javascript world, there is the MSW package which lets you define if it should match all the routes or just the defined ones. It's a common usecase for development to only want to mock a new (non-existent) endpoint in order to already develop without having a working backend and let the other routes go to the existing back-end as usual.

Such a feature would increase the value of this package by a lot! Is it possible to add such a config parameter or would it require much more work?

Thanks, Sebastian for the suggestion!
I think what @HeLinChooi mentioned is the proper way to achieve that result. What else can be added/improved?

@sebastianbuechler
Copy link
Collaborator

You mean to have some real routes and some mocked ones together ?

In the Javascript world, there is the MSW package which lets you define if it should match all the routes or just the defined ones. It's a common usecase for development to only want to mock a new (non-existent) endpoint in order to already develop without having a working backend and let the other routes go to the existing back-end as usual.
Such a feature would increase the value of this package by a lot! Is it possible to add such a config parameter or would it require much more work?

Thanks, Sebastian for the suggestion! I think what @HeLinChooi mentioned is the proper way to achieve that result. What else can be added/improved?

I think the proposed solution @HeLinChooi is not optimal as it requires the ability to switch out the DIO object for a single call in my proposed usecase. Let's say you have a service with 10 calls and you want to add a new call there. Then you would have to integrate the mocked DIO object to the service, add the handler for the mock and build the new call on top of that.

In the approach of MSW you will always use the same DIO object and if the call does not match to any mocked handler it will just be called as usual with the real DIO object.

At the moment here is the condition if the route is not matched for a call and it always throws an exception. For development usage here it would be super nice to have a settings flag in order to choose: a) throw exception or b) route call to original DIO object.

Do you see my point?

@sebastianbuechler
Copy link
Collaborator

sebastianbuechler commented May 26, 2023

I implemented a simple selective mocking class like this:

import 'package:dio/dio.dart';
import 'package:kasparund/mocks/mock_handlers.dart';

/// Interceptor for mocking API responses
///
/// This interceptor will intercept all requests and check if there is a mock
/// response for the request. If there is, it will return the mock response.
/// Otherwise, it will continue with the request as normal.
///
/// The mock responses are defined in [mockRoutes].
///
/// The [apiBaseURL] is used to match the request URL to the mock response.
///
/// Usage:
///
/// ```dart
/// final dio = Dio();
/// dio.interceptors.add(
///  DioMockInterceptor(apiBaseURL: 'https://example.com'),
/// );
/// ```
///
/// See also:
///
/// * [mockRoutes], which contains the mock responses
class DioMockInterceptor extends Interceptor {
  final String apiBaseURL;

  DioMockInterceptor({required this.apiBaseURL});

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    final routeKey = '${options.method}:${options.path}';
    if (mockRoutes.containsKey(routeKey)) {
      final response = mockRoutes[routeKey]!(options);
      handler.resolve(response);
    } else {
      handler.next(options);
    }
  }
}

You can then define the routes here:

import 'package:dio/dio.dart';

/// Mocked routes for testing
///
/// This is a map of routes to mocked responses. The key is a string
/// in the format of `METHOD:PATH`, where `METHOD` is the HTTP method
/// (e.g. `GET`, `POST`, etc.) and `PATH` is the path of the request
/// (e.g. `api/users`, `api/users/123`, etc.). 
///
/// The value is a function that takes a `RequestOptions` object and
/// returns a `Response` object. The `RequestOptions` object contains
/// the request information, such as the HTTP method, path, headers,
/// and body. The `Response` object contains the response information,
/// such as the status code and body.
///
/// For example, the following route will return a 200 response with
/// the body `{'message': 'Mocked GET response'}` for any `GET` request
/// to the path `/test`:
///
/// ```dart
/// 'GET:test': (RequestOptions options) {
///  return Response(
///   requestOptions: options,
///  data: {'message': 'Mocked GET response'},
/// statusCode: 200,
/// );
/// ```
final Map<String, Response Function(RequestOptions)> mockRoutes = {
  'GET:test/123': (RequestOptions options) {
    return Response(
      requestOptions: options,
      data: {'message': 'Mocked GET response'},
      statusCode: 200,
    );
  },
  // Add more routes as needed
};

And then add it to the dio instance like this:

  // Add mock handlers if in debug mode
  if (kDebugMode) {
    dio.interceptors.add(DioMockInterceptor(apiBaseURL: config.apiBaseURL));
  }

Really like it for development.

@LukaGiorgadze
Copy link
Member

I implemented a simple selective mocking class like this:

import 'package:dio/dio.dart';
import 'package:kasparund/mocks/mock_handlers.dart';

/// Interceptor for mocking API responses
///
/// This interceptor will intercept all requests and check if there is a mock
/// response for the request. If there is, it will return the mock response.
/// Otherwise, it will continue with the request as normal.
///
/// The mock responses are defined in [mockRoutes].
///
/// The [apiBaseURL] is used to match the request URL to the mock response.
///
/// Usage:
///
/// ```dart
/// final dio = Dio();
/// dio.interceptors.add(
///  DioMockInterceptor(apiBaseURL: 'https://example.com'),
/// );
/// ```
///
/// See also:
///
/// * [mockRoutes], which contains the mock responses
class DioMockInterceptor extends Interceptor {
  final String apiBaseURL;

  DioMockInterceptor({required this.apiBaseURL});

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    final routeKey = '${options.method}:${options.path}';
    if (mockRoutes.containsKey(routeKey)) {
      final response = mockRoutes[routeKey]!(options);
      handler.resolve(response);
    } else {
      handler.next(options);
    }
  }
}

You can then define the routes here:

import 'package:dio/dio.dart';

/// Mocked routes for testing
///
/// This is a map of routes to mocked responses. The key is a string
/// in the format of `METHOD:PATH`, where `METHOD` is the HTTP method
/// (e.g. `GET`, `POST`, etc.) and `PATH` is the path of the request
/// (e.g. `api/users`, `api/users/123`, etc.). 
///
/// The value is a function that takes a `RequestOptions` object and
/// returns a `Response` object. The `RequestOptions` object contains
/// the request information, such as the HTTP method, path, headers,
/// and body. The `Response` object contains the response information,
/// such as the status code and body.
///
/// For example, the following route will return a 200 response with
/// the body `{'message': 'Mocked GET response'}` for any `GET` request
/// to the path `/test`:
///
/// ```dart
/// 'GET:test': (RequestOptions options) {
///  return Response(
///   requestOptions: options,
///  data: {'message': 'Mocked GET response'},
/// statusCode: 200,
/// );
/// ```
final Map<String, Response Function(RequestOptions)> mockRoutes = {
  'GET:test/123': (RequestOptions options) {
    return Response(
      requestOptions: options,
      data: {'message': 'Mocked GET response'},
      statusCode: 200,
    );
  },
  // Add more routes as needed
};

And then add it to the dio instance like this:

  // Add mock handlers if in debug mode
  if (kDebugMode) {
    dio.interceptors.add(DioMockInterceptor(apiBaseURL: config.apiBaseURL));
  }

Really like it for development.

I would be more than happy if you could make a PR 💟

@sebastianbuechler
Copy link
Collaborator

sebastianbuechler commented May 28, 2023

I implemented a simple selective mocking class like this:

import 'package:dio/dio.dart';
import 'package:kasparund/mocks/mock_handlers.dart';

/// Interceptor for mocking API responses
///
/// This interceptor will intercept all requests and check if there is a mock
/// response for the request. If there is, it will return the mock response.
/// Otherwise, it will continue with the request as normal.
///
/// The mock responses are defined in [mockRoutes].
///
/// The [apiBaseURL] is used to match the request URL to the mock response.
///
/// Usage:
///
/// ```dart
/// final dio = Dio();
/// dio.interceptors.add(
///  DioMockInterceptor(apiBaseURL: 'https://example.com'),
/// );
/// ```
///
/// See also:
///
/// * [mockRoutes], which contains the mock responses
class DioMockInterceptor extends Interceptor {
  final String apiBaseURL;

  DioMockInterceptor({required this.apiBaseURL});

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    final routeKey = '${options.method}:${options.path}';
    if (mockRoutes.containsKey(routeKey)) {
      final response = mockRoutes[routeKey]!(options);
      handler.resolve(response);
    } else {
      handler.next(options);
    }
  }
}

You can then define the routes here:

import 'package:dio/dio.dart';

/// Mocked routes for testing
///
/// This is a map of routes to mocked responses. The key is a string
/// in the format of `METHOD:PATH`, where `METHOD` is the HTTP method
/// (e.g. `GET`, `POST`, etc.) and `PATH` is the path of the request
/// (e.g. `api/users`, `api/users/123`, etc.). 
///
/// The value is a function that takes a `RequestOptions` object and
/// returns a `Response` object. The `RequestOptions` object contains
/// the request information, such as the HTTP method, path, headers,
/// and body. The `Response` object contains the response information,
/// such as the status code and body.
///
/// For example, the following route will return a 200 response with
/// the body `{'message': 'Mocked GET response'}` for any `GET` request
/// to the path `/test`:
///
/// ```dart
/// 'GET:test': (RequestOptions options) {
///  return Response(
///   requestOptions: options,
///  data: {'message': 'Mocked GET response'},
/// statusCode: 200,
/// );
/// ```
final Map<String, Response Function(RequestOptions)> mockRoutes = {
  'GET:test/123': (RequestOptions options) {
    return Response(
      requestOptions: options,
      data: {'message': 'Mocked GET response'},
      statusCode: 200,
    );
  },
  // Add more routes as needed
};

And then add it to the dio instance like this:

  // Add mock handlers if in debug mode
  if (kDebugMode) {
    dio.interceptors.add(DioMockInterceptor(apiBaseURL: config.apiBaseURL));
  }

Really like it for development.

I would be more than happy if you could make a PR 💟

I tried at first, but I think it would require some major adjustments to the package, as currently, it will replace some core functionality of dio completely by overriding the HttpClientAdapter .

I saw that in here we could add the feature if a non-matched call should throw an error or go back to original dio, but RequestInterceptorHandler is not available, so we can not call it to "jump over" the mocking.

If somebody has an idea on how to combine those two approaches I would be happy to try again.

One thing I saw in the MSW library: They use two different approaches for unit testing and development mocking, but maybe that's also because of the browser vs node.js implementation. However, we could think here as well, if we want a second approach that's more focused on development mocking instead of unit testing.

@eggzotic
Copy link

eggzotic commented Aug 6, 2023

I've been looking for a solution to this recently - and now found this thread - great to know I am not alone in wishing for this :-). I support the request to have this combined real & mock API capability in the same Dio instance - essentially it behaves as a real Dio (making requests to the remote endpoint) except when a mock-override is defined. The partial solution that @sebastianbuechler offers gave me an idea - I have adapted it so that the mocked routes can be specified in a JSON file that is loaded via assets. There is a demo of it here: https://github.com/eggzotic/mock_some_demo. It means that when you've completed all your mocking and all routes are now "real", all you have to do is remove the mocked routes from config.json (leaving its content as an empty {}) - no further mods required.

@sebastianbuechler
Copy link
Collaborator

Added the ability for selective mocking via #159 and will be available in the next release.

Usage:
DioInterceptor(dio: dio, failOnMissingMock: false);

Be aware that this flag is only available through the DioInterceptor class and not on DioAdapter for technical reasons.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants