MSW Dio Interceptor

Lint and Test Status Release Status Pub Version License: MIT

Heavily inspired by Mock Service Worker (MSW), this is a powerful and flexible mock interceptor for dio, designed for Dart and Flutter applications. It enables you to intercept dio requests and return simulated responses based on configurable rules, making it perfect for testing, development, and UI prototyping without relying on a running backend.

Table of Contents

Features

  • Clean Architecture: A universal core engine with a dedicated adapter for Dio.
  • Easy to Use: Simple and intuitive API for registering mock rules.
  • Environment-Controlled: Enable or disable mocks globally by conditionally adding the interceptor.
  • Flexible Matching: Match requests by path, full URL, RegExp, and query parameters.
  • Realistic Simulations: Simulate status codes, errors, delays, and custom headers.
  • Customizable Logging: Built-in logging for mocked requests, with custom print function support.

Installation

Add the package to your pubspec.yaml:

dependencies:
  dio: <latest_version>
  
dev_dependencies:
  msw_dio_interceptor: <latest_version>

Then, run dart pub get or flutter pub get.

How to Use

1. Enable Mocks via Environment Flag

To enable mocks, you will conditionally add the MockInterceptor to your Dio instance based on an environment flag.

# For pure Dart apps
dart --define=ENABLE_API_MOCK=true run your_app.dart

# For Flutter apps
flutter run --dart-define=ENABLE_API_MOCK=true

2. Register Mock Rules

Use the MockRegistry to define the rules for your mock responses. This is typically done once at application startup or before your tests run.

import 'package:msw_dio_interceptor/msw_dio_interceptor.dart';

void setupMocks() {
  MockRegistry.register(
    MockRule(
      path: '/products',
      method: 'GET',
      handler: (request) {
        return MockResponse.json({
          'items': [{'id': 1, 'name': 'Product A'}]
        });
      },
    ),
  );
}

3. Conditionally Add the Interceptor to Dio

Create an instance of the MockHttpEngine and the MockInterceptor. Then, conditionally add the MockInterceptor to Dio's interceptors list based on your environment flag.

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

const bool kEnableApiMock = bool.fromEnvironment('ENABLE_API_MOCK');

// 1. Create the engine (it no longer has an 'enabled' flag)
final mockEngine = MockHttpEngine();

// 2. Create a Dio instance and add the interceptor
final dio = Dio();

// 3. Conditionally add the interceptor
if (kEnableApiMock) {
  dio.interceptors.add(MockInterceptor(engine: mockEngine));
}

// Now you can use dio as usual
await dio.get('/products');

Mocking Capabilities

The MockRule class offers flexible ways to match incoming requests and define their responses.

Basic Path Matching

Match requests based on a specific path and HTTP method.

MockRegistry.register(
  MockRule(
    path: '/users',
    method: 'GET',
    handler: (request) => MockResponse.json([
      {'id': 1, 'name': 'Alice'},
      {'id': 2, 'name': 'Bob'},
    ]),
  ),
);

Matching by Regular Expression (RegExp)

Use MockRule.regex for dynamic paths, such as fetching resources by ID.

MockRegistry.register(
  MockRule.regex(
    pattern: r'\/users\/\d+', // Matches /users/1, /users/123, etc.
    method: 'GET',
    handler: (request) {
      final userId = request.url.split('/').last;
      return MockResponse.json({'id': userId, 'name': 'User $userId'});
    },
  ),
);

Matching by Full URL

Use MockRule.url to match an exact URL, including the domain, protocol, and path.

MockRegistry.register(
  MockRule.url(
    url: 'https://api.example.com/health',
    method: 'GET',
    handler: (request) => MockResponse.text('OK - API is healthy'),
  ),
);

Matching with Query Parameters

The queryParams property allows you to match requests only if they contain specific query parameters.

MockRegistry.register(
  MockRule(
    path: '/search',
    method: 'GET',
    queryParams: {'q': 'flutter', 'page': '1'},
    handler: (request) => MockResponse.json({'results': ['Flutter rocks!']}),
  ),
);

Mocking Different HTTP Methods

Define separate rules for different HTTP methods (GET, POST, PUT, DELETE, etc.) to the same path.

// GET /items
MockRegistry.register(
  MockRule(
    path: '/items',
    method: 'GET',
    handler: (request) => MockResponse.json({'items': []}),
  ),
);

// POST /items
MockRegistry.register(
  MockRule(
    path: '/items',
    method: 'POST',
    handler: (request) => MockResponse.json({'message': 'Item created'}, statusCode: 201),
  ),
);

Simulating Network Latency (Delay)

Use the delayMs property in MockRule to simulate network latency for a specific mock response.

MockRegistry.register(
  MockRule(
    path: '/slow-data',
    method: 'GET',
    delayMs: 2000, // Delays the response by 2 seconds
    handler: (request) => MockResponse.json({'data': 'This came after a delay!'}),
  ),
);

Mocking Error Responses

To simulate an error, simply provide a statusCode of 400 or higher in your MockResponse.

MockRegistry.register(
  MockRule(
    path: '/auth/login',
    method: 'POST',
    handler: (request) => MockResponse.json(
      {'error': 'Invalid credentials'},
      statusCode: 401,
    ),
  ),
);

Mocking Different Response Body Types

Use the factory constructors of MockResponse to easily create JSON, text, or byte array responses.

// JSON Response (default for .json factory)
MockRegistry.register(
  MockRule(
    path: '/data',
    method: 'GET',
    handler: (request) => MockResponse.json({'key': 'value'}),
  ),
);

// Text Response
MockRegistry.register(
  MockRule(
    path: '/status',
    method: 'GET',
    handler: (request) => MockResponse.text('Service is operational'),
  ),
);

// Bytes Response (e.g., for images or binary data)
import 'dart:typed_data';
MockRegistry.register(
  MockRule(
    path: '/image',
    method: 'GET',
    handler: (request) => MockResponse.bytes(Uint8List.fromList([0x89, 0x50, 0x4E, 0x47])), // PNG header example
  ),
);

Logging Mocked Requests

You can enable logging for mocked requests to see details directly in your console. This is useful for debugging and understanding which mocks are being hit.

To enable logging, set the log parameter to true when creating the MockInterceptor. You can also provide a custom logPrint function.

import 'package:flutter/foundation.dart'; // For debugPrint in Flutter

// ...
final dio = Dio();
dio.interceptors.add(
  MockInterceptor(
    engine: mockEngine,
    log: true, // Enable logging
    // Optional: Provide a custom logPrint function (defaults to print)
    // logPrint: debugPrint, // Use debugPrint in Flutter apps
  ),
);

Example log output:

╔══ 🚀 Mocked Request ══╗
║ URI: http://example.com/products
║ Method: GET
║
╟── Mock Response ───
║ Status: 200
║ Data: {"items":[{"id":1,"name":"Mock Product"}]}
╚════════════════════╝

Testing with Mocks

A primary use case for this package is to write clean and reliable tests for your application's data layer (e.g., repositories or data sources) without making real network requests.

Philosophy

The goal is to test your data layer's logic (how it handles success, errors, and data parsing) without depending on a live server. By mocking the server response at the network level, your data source class doesn't need to know it's being tested.

Example: Testing a Repository

Imagine you have a ProductRepository that fetches products from an API.

// Your repository class
class ProductRepository {
  final Dio _dio;
  ProductRepository(this._dio);

  Future<List<Product>> fetchProducts() async {
    try {
      final response = await _dio.get('/products');
      final items = (response.data['items'] as List);
      return items.map((item) => Product.fromJson(item)).toList();
    } catch (e) {
      // In a real app, you'd have more robust error handling
      throw Exception('Failed to fetch products');
    }
  }
}

class Product {
  final int id;
  final String name;
  Product({required this.id, required this.name});
  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(id: json['id'], name: json['name']);
  }
}

Now, let's write a test for this repository.

// In your test file
import 'package:test/test.dart';
import 'package:dio/dio.dart';
import 'package:msw_dio_interceptor/msw_dio_interceptor.dart';

void main() {
  late Dio dio;
  late MockHttpEngine mockEngine;

  setUp(() {
    // 1. Create the mock engine, enabled for tests
    mockEngine = MockHttpEngine();
    
    // 2. Create a new Dio instance for each test and add the interceptor
    dio = Dio();
    dio.interceptors.add(MockInterceptor(engine: mockEngine));

    // 3. Clear all mocks before each test to ensure isolation
    MockRegistry.clear();
  });

  test('fetchProducts returns a list of products on success', () async {
    // Arrange: Define the mock response for this specific test
    MockRegistry.register(
      MockRule(
        path: '/products',
        method: 'GET',
        handler: (request) => MockResponse.json({
          'items': [
            {'id': 1, 'name': 'Mock Product 1'},
            {'id': 2, 'name': 'Mock Product 2'},
          ]
        }),
      ),
    );

    // Act: Call the method you want to test
    final response = await dio.get('/products');
    final products = (response.data['items'] as List)
        .map((item) => Product.fromJson(item))
        .toList();

    // Assert: Verify the result
    expect(products, isA<List<Product>>());
    expect(products.length, 2);
    expect(products.first.name, 'Mock Product 1');
  });

  test('fetchProducts throws an exception on server error', () async {
    // Arrange: Mock a server error response
    MockRegistry.register(
      MockRule(
        path: '/products',
        method: 'GET',
        handler: (request) => MockResponse.json(
          {'error': 'Internal Server Error'},
          statusCode: 500,
        ),
      ),
    );

    // Act & Assert: Verify that the correct exception is thrown
    expect(
      () => productRepository.fetchProducts(),
      throwsA(isA<Exception>()),
    );
  });
}

Contributing

Contributions are welcome! Please feel free to open an issue or submit a pull request.

Changelog

1.0.0

  • Initial release.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Libraries

msw_dio_interceptor
A powerful mock interceptor for Dio, inspired by MSW.