search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs
You are viewing an older version (1.0.0-alpha.10). Go to the latest.

HTTP Tests

Introduction

Magic provides a first-class HTTP faking API that lets you swap the real network driver with a FakeNetworkDriver during tests. All requests are recorded and no real network traffic is made. This approach is inspired by Laravel's Http::fake() and requires no third-party mocking libraries.

setUp(() {
  MagicApp.reset();
  Magic.flush();

  Http.fake(); // Intercept all HTTP requests
});

tearDown(() {
  Http.unfake(); // Restore real driver
});

Faking Responses

Call Http.fake() before the code under test runs. When called with no arguments, every request returns a 200 response with empty data.

final fake = Http.fake();

final response = await Http.get('/users');

expect(response.successful, isTrue);
expect(response.data, equals({}));

URL Pattern Stubs

Pass a Map to stub specific URL patterns. Patterns support * as a wildcard.

final fake = Http.fake({
  'users/*': Http.response({'id': 1, 'name': 'Alice'}, 200),
  'posts': Http.response([], 200),
  'auth/login': Http.response({'token': 'test-token'}, 200),
});

final user = await Http.get('/users/42');
expect(user['name'], 'Alice');

final posts = await Http.get('/posts');
expect(posts.data, isEmpty);

Pattern matching is done after stripping the leading / so both /users/42 and users/42 match the pattern users/*.

Callback Stubs

Pass a FakeRequestHandler — a function that receives a MagicRequest and returns a MagicResponse — for dynamic stubbing logic.

final fake = Http.fake((request) {
  if (request.url.contains('admin')) {
    return Http.response({'error': 'Forbidden'}, 403);
  }

  return Http.response({'ok': true}, 200);
});

final adminResponse = await Http.get('/admin/users');
expect(adminResponse.forbidden, isTrue);

final publicResponse = await Http.get('/users');
expect(publicResponse.successful, isTrue);

Making Assertions

Http.fake() returns the FakeNetworkDriver instance. Use it to assert on recorded requests after the code under test runs.

assertSent

Assert that at least one recorded request matches the predicate.

final fake = Http.fake();

await Http.post('/users', data: {'name': 'Bob'});

fake.assertSent((request) => request.url.contains('users'));
fake.assertSent(
  (request) => request.method == 'POST' && request.url == '/users',
);

assertNotSent

Assert that no recorded request matches the predicate.

fake.assertNotSent((request) => request.url.contains('payments'));

assertNothingSent

Assert that no requests were made at all.

final fake = Http.fake();

// Code that should not touch the network
doSomethingLocal();

fake.assertNothingSent();

assertSentCount

Assert an exact number of requests were recorded.

final fake = Http.fake();

await Http.get('/users');
await Http.get('/users/1');

fake.assertSentCount(2);

recorded

Access the full list of recorded (MagicRequest, MagicResponse) pairs for custom assertions.

final fake = Http.fake();

await Http.post('/orders', data: {'item': 'Widget'});

final pair = fake.recorded.first;
expect(pair.$1.method, 'POST');
expect(pair.$2.statusCode, 200);

Preventing Stray Requests

Call preventStrayRequests() on the fake driver to throw a StrayRequestException whenever an unmatched request is made. This ensures every HTTP call in your test has an explicit stub.

final fake = Http.fake({
  'users': Http.response([], 200),
})..preventStrayRequests();

// This is fine — matched by stub
await Http.get('/users');

// This throws StrayRequestException — no matching stub
await Http.get('/notifications'); // throws!

[!TIP] Enable preventStrayRequests() in tests that should be fully isolated from external services to catch accidental network calls early.

Response Factory

Http.response() is a convenience factory for building MagicResponse objects to use as stub responses.

// 200 with empty data (default)
Http.response()

// 200 with a Map body
Http.response({'id': 1, 'name': 'Alice'})

// Custom status code
Http.response({'message': 'Not found'}, 404)

// List body
Http.response([{'id': 1}, {'id': 2}], 200)

// 422 validation errors
Http.response({
  'message': 'The given data was invalid.',
  'errors': {
    'email': ['The email field is required.'],
  },
}, 422)

Unfaking

Call Http.unfake() in tearDown to remove the fake driver from the IoC container and restore the real network driver for subsequent tests.

tearDown(() {
  Http.unfake();
});

Alternatively, you can call fake.reset() on the driver instance to clear recorded requests and stubs without restoring the real driver. This is useful when reusing the same fake across multiple test cases.

setUp(() {
  MagicApp.reset();
  Magic.flush();
});

// In a group where all tests share one fake:
final fake = Http.fake();

test('first test', () async {
  await Http.get('/a');
  fake.assertSentCount(1);
  fake.reset(); // Clear for next test
});

test('second test', () async {
  await Http.get('/b');
  fake.assertSentCount(1);
});

Full Example

A controller that fetches a user profile and the test verifying it:

// lib/controllers/profile_controller.dart
class ProfileController extends MagicController
    with MagicStateMixin>, ValidatesRequests {
  Future load(String userId) async {
    setLoading();

    final response = await Http.show('users', userId);

    if (response.successful) {
      setSuccess(response.data as Map);
    } else {
      setError(response.firstError ?? 'Failed to load profile');
    }
  }
}

// test/http/profile_controller_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';

void main() {
  group('ProfileController', () {
    late ProfileController controller;
    late FakeNetworkDriver fake;

    setUp(() {
      MagicApp.reset();
      Magic.flush();

      controller = ProfileController();
      Magic.put(controller);

      fake = Http.fake();
    });

    tearDown(() {
      Http.unfake();
    });

    test('sets success state on 200 response', () async {
      fake.stub(
        'users/*',
        Http.response({'id': '42', 'name': 'Alice'}, 200),
      );

      await controller.load('42');

      expect(controller.isSuccess, isTrue);
      expect(controller.rxState?['name'], 'Alice');
      fake.assertSent((r) => r.url.contains('users/42'));
    });

    test('sets error state on failure', () async {
      fake.stub(
        'users/*',
        Http.response({'message': 'User not found'}, 404),
      );

      await controller.load('999');

      expect(controller.isError, isTrue);
    });

    test('does not call network when userId is empty', () async {
      // No real call should be made
      fake.assertNothingSent();
    });
  });
}

Testing fetchList and fetchOne

Controllers that use fetchList() or fetchOne() from MagicStateMixin can be tested with Http.fake() the same way as any other HTTP call. Stub the URL the helper will request, then assert on the resulting controller state.

fetchList

// lib/controllers/project_controller.dart
class ProjectController extends MagicController
    with MagicStateMixin> {
  Future loadProjects(String teamId) =>
      fetchList('teams/$teamId/projects', Project.fromMap);
}

// test/http/project_controller_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';

void main() {
  group('ProjectController.fetchList', () {
    late ProjectController controller;
    late FakeNetworkDriver fake;

    setUp(() {
      MagicApp.reset();
      Magic.flush();

      controller = ProjectController();
      Magic.put(controller);

      fake = Http.fake({
        'teams/*/projects': Http.response({
          'data': [
            {'id': 1, 'name': 'Project A'},
            {'id': 2, 'name': 'Project B'},
          ],
        }, 200),
      });
    });

    tearDown(() => Http.unfake());

    test('sets success state with parsed list', () async {
      await controller.loadProjects('team-1');

      expect(controller.isSuccess, isTrue);
      expect(controller.rxState?.length, 2);
      expect(controller.rxState?.first.name, 'Project A');
      fake.assertSent((r) => r.url.contains('teams/team-1/projects'));
    });

    test('sets empty state when data list is empty', () async {
      fake.stub('teams/*/projects', Http.response({'data': []}, 200));

      await controller.loadProjects('team-1');

      expect(controller.isEmpty, isTrue);
    });

    test('sets error state on failed response', () async {
      fake.stub(
        'teams/*/projects',
        Http.response({'message': 'Unauthorized'}, 401),
      );

      await controller.loadProjects('team-1');

      expect(controller.isError, isTrue);
    });
  });
}

fetchOne

// lib/controllers/project_detail_controller.dart
class ProjectDetailController extends MagicController
    with MagicStateMixin {
  Future loadProject(String id) =>
      fetchOne('projects/$id', Project.fromMap);
}

// test/http/project_detail_controller_test.dart
void main() {
  group('ProjectDetailController.fetchOne', () {
    late ProjectDetailController controller;
    late FakeNetworkDriver fake;

    setUp(() {
      MagicApp.reset();
      Magic.flush();

      controller = ProjectDetailController();
      Magic.put(controller);

      fake = Http.fake({
        'projects/*': Http.response({
          'data': {'id': 1, 'name': 'Project A'},
        }, 200),
      });
    });

    tearDown(() => Http.unfake());

    test('sets success state with parsed model', () async {
      await controller.loadProject('1');

      expect(controller.isSuccess, isTrue);
      expect(controller.rxState?.name, 'Project A');
      fake.assertSent((r) => r.url.contains('projects/1'));
    });

    test('sets error state when data key is absent', () async {
      fake.stub('projects/*', Http.response({'meta': {}}, 200));

      await controller.loadProject('1');

      expect(controller.isError, isTrue);
    });
  });
}

[!TIP] Use fake.stub() inside individual tests to override the default stub registered in setUp. This keeps the happy path in setUp and edge cases inline.

See Also

  • Facade TestingAuth.fake(), Cache.fake(), Vault.fake(), Log.fake()