Simulating HTTP request/response workflow for effective testing in Dart/Flutter via http-mock-adapter

campfire5

campfire5

Posted on November 23, 2020

Simulating HTTP request/response workflow for effective testing in Dart/Flutter via http-mock-adapter

Alt Text

http-mock-adapter is a Dart package built upon Dio & Mockito. By simply defining requests and corresponding responses through predefined adapters, you will be able to mock requests sent via Dio. Alternatively, you can simply mock the data just as before via Mockito on http-mock-adapter's custom adapters.

The problem

What problem does http-mock-adapter solve?

The problem lies within the nature of the issue - efficient testing.

Generally speaking, a project might include a lot of code that:

  • Depends on an external service (network, database)
  • Takes hefty amount of time to compute
  • Depends on many existing entities and conditions within its structure
  • Does not satisfy all of the predefined functionality
  • May change in the future

More specifically, in the case of making HTTP requests, is it necessary to send real data to real server and infer real response?

Making a network request takes time. It is also not guaranteed that the server will be up and running at all times. Then there are problems with sending too much requests, especially just for the sake of testing some parts of the software.

http-mock-adapter aims to solve problems associated with interacting with a real server.

The solution

Universally accepted solution for testing resource intensive tasks is data mocking. Mock data is an artificial data used to simulate the workflow of real processes, most often as a part of a software testing initiative.

In layman's terms - developers/testers mock objects to bypass object's concrete implementations, resulting in custom behavior; for example:

Suppose we want to test a UI of an application's sign up functionality - we may have to provide registration forms with email and password, click the Sign Up button to signal the application to process user input and then await the result of the registration process in order to render appropriate elements on the screen.

Provided that the application definitely connects to a remote server for registration purposes and might also include lots of small dependencies (caching user input locally, sending log data to server, etc.) just to successfully send the registration request to the server, - what can go wrong? Numerous undesirable outcomes may occur:

  • Error due to unsupported user input parsing
  • Error due to server being down
  • Error due to lack of dependency injection (in case that the sign up functionality depends on other entities of the software)
  • Having to make new unique user each time you want to run the test
  • Having to wait more than it is necessary to test whether or not application's UI works as defined as a result of success/failure/delay of registration
  • Sending extra, unnecessary requests to server and slowing it down
  • Having to wait a long time for the tests to complete if you have a lot of tests that do not utilize mocking
  • etc.

Data mocking solves all of the aforementioned problems with very simple usage.

You can instruct the test to create the mock implementation of the real sign up functionality, define custom data, HTTP client, HTTP request & response, and so on, with a logic similar to this: "You want to register new user? I will create a mock object that will generate artificial response of "Successfully registered!" if you send any kind of data to this specific /signup route!"

We can demonstrate the ease of use of http-mock-adapter with intent to return specific response as a reply to a specific request:

import 'package:dio/dio.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';

void main() async {
  final dio = Dio();
  final dioAdapter = DioAdapter();

  dio.httpClientAdapter = dioAdapter;

  const path = 'https://example.com';

  dioAdapter
      .onGet(path)
      .reply(200, {'message': 'Successfully mocked GET!'})
      .onPost(path)
      .reply(200, {'message': 'Successfully mocked POST!'});

  final onGetResponse = await dio.get(path);
  print(onGetResponse.data); // {"message":"Successfully mocked GET!"}

  final onPostResponse = await dio.post(path);
  print(onPostResponse.data); // {"message":"Successfully mocked POST!"}
}
Enter fullscreen mode Exit fullscreen mode

With such simple steps, we can instruct Dio (the HTTP client) with our custom DioAdapter (class that exposes developer-friendly methods to mock requests) to mock GET and POST requests aimed at https://example.com with predefined status codes, data, and more!

No server issues, blazing fast operations, super easy to use, no worrying about dependencies and implementation details, total control over how we want to test our application based on our needs and preferences.

Alternatively, you can use our DioInterceptor to mock Dio requests with our custom interceptor by adding it inside the dio.interceptors list:

import 'package:dio/dio.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';

void main() async {
  final dio = Dio();
  final dioInterceptor = DioInterceptor();

  dio.interceptors.add(dioInterceptor);

  const path = 'https://example.com';

  dioInterceptor
      .onDelete(path)
      .reply(200, {'message': 'Successfully mocked DELETE!'})
      .onPatch(path)
      .reply(200, {'message': 'Successfully mocked POST!'});

  final deleteResponse = await dio.delete(path);
  print(deleteResponse.data); // {"message":"Successfully mocked DELETE!"}

  final postResposne = await dio.patch(path);
  print(postResposne.data); // {"message":"Successfully mocked POST!"}
}
Enter fullscreen mode Exit fullscreen mode

Bonus: what if you are used to mocking with Mockito? http-mock-adapter includes mocked version of DioAdapter ready to be used:

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:test/test.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:mockito/mockito.dart';

void main() {
  Dio dio;
  Response<dynamic> response;

  group('DioAdapterMockito', () {
    DioAdapterMockito dioAdapterMockito;

    setUpAll(() {
      dio = Dio();

      dioAdapterMockito = DioAdapterMockito();

      dio.httpClientAdapter = dioAdapterMockito;
    });

    test('mocks any request/response via fetch method', () async {
      final responsePayload = jsonEncode({'response_code': '200'});

      final responseBody = ResponseBody.fromString(
        responsePayload,
        200,
        headers: {
          Headers.contentTypeHeader: [Headers.jsonContentType],
        },
      );

      final expected = {'response_code': '200'};

      when(dioAdapterMockito.fetch(any, any, any))
          .thenAnswer((_) async => responseBody);

      response = await dio.get('/route');

      expect(expected, response.data);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Regarding the package

The package is being developed on a per-need basis at Lomsa. We could not find a decent Dart package to easily mock HTTP requests, so we decided to make one.

The package has reached status where it can already be used to mock Dio's requests. We plan to continue working on it and make it even better to use, with additional functionalities, null safety, clean and easy-to-use API, optimized code, integration with other HTTP clients and much more!

Closing thoughts

Data mocking is an important concept that is very useful to know for any developer that is serious about having a thoroughly tested codebase to ensure that the software functions as well as it can.

We hope that you give http-mock-adapter a shot! If you find it useful, please 👍 the package, spread the word, and reach out to us at the issues' section if you encounter a bug or think of an awesome feature to improve the package!

💖 💪 🙅 🚩
campfire5
campfire5

Posted on November 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related