# Testing: Getting Started

Magic is built with testing in mind: it integrates with Dart's `test` package and provides helpers for resetting the IoC container, faking facades, and asserting controller and model behavior.

- [Introduction](#introduction)
- [Setting Up Tests](#setting-up-tests)
- [Writing Tests](#writing-tests)
- [Controller Testing](#controller-testing)
- [Model Testing](#model-testing)
- [Mock HTTP Responses](#mock-http-responses)
- [Running Tests](#running-tests)

## Introduction

Magic is built with testing in mind from the ground up. The framework provides a testing foundation that works seamlessly with Dart's built-in `test` package, along with helpers for testing controllers, models, and HTTP interactions.

## Setting Up Tests

### Directory Structure

```
test/
├── unit/
│   ├── models/
│   │   └── user_test.dart
│   └── controllers/
│       └── auth_controller_test.dart
├── feature/
│   └── authentication_test.dart
└── test_helper.dart
```

### Test Helper

Magic provides a `MagicTest` bootstrap helper via `package:magic/testing.dart`. Call `MagicTest.init()` once at the top of `main()` — it registers `setUpAll`, `setUp`, and `tearDown` automatically:

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/testing.dart';

void main() {
  MagicTest.init(); // Registers setUpAll + setUp + tearDown automatically

  test('my test', () {
    // Magic container is clean, facades are reset
  });
}
```

`MagicTest.init()` handles:
- `setUpAll`: `TestWidgetsFlutterBinding.ensureInitialized()`
- `setUp`: `MagicApp.reset()` + `Magic.flush()` + `Gate.flush()`
- `tearDown`: `Magic.flush()` + `Gate.flush()` + `DateManager.reset()` + `Translator.reset()`, so a loaded catalogue or locale does not carry into the next test

`Gate` is a process static that `Magic.flush()` does not clear on its own, so without the extra call an ability defined in one test leaks into every later test in the same file.

For integration tests that need a full `Magic.init()` lifecycle, use `MagicTest.boot()` instead:

```dart
import 'package:magic/testing.dart';

void main() {
  setUpAll(() async {
    await MagicTest.boot(
      configs: [testDatabaseConfig],
      envFileName: '.env.testing',
    );
  });

  test('full lifecycle test', () async {
    // Magic is fully initialised with providers booted
  });
}

Map<String, dynamic> get testDatabaseConfig => {
  'database': {
    'default': 'sqlite',
    'connections': {
      'sqlite': {
        'driver': 'sqlite',
        'database': ':memory:', // In-memory for tests
      },
    },
  },
};
```

### Booting a Handful of Providers

A widget test that needs real providers but no env file or config boots them with `MagicTest.bootProviders(providers)`. It reproduces `Magic.init`'s order: every `register()` first, then every `boot()`, both in list order. Pair it with `MagicTest.disposeBooted()` in `tearDown`:

```dart
setUp(() async {
  await MagicTest.bootProviders([
    AppServiceProvider(Magic.app),
    AuthServiceProvider(Magic.app),
  ]);
});

tearDown(MagicTest.disposeBooted);
```

`disposeBooted()` undoes what `Magic.flush()` does not reach: it disposes the registered controllers (newest first), detaches `SessionScope` from auth state, resets `MagicRouter`, then flushes the container. Anything an app keeps in its own statics, such as a timer a provider arms in `boot()`, stays the app's to reset.

### Loading a Translation Catalogue

A widget test that renders `trans()` output needs a real catalogue loaded first; without one, `trans()` renders the raw dotted key. `MagicTest.loadTranslations(locale, {directory})` reads `<directory>/<locale>.json` off disk (default `assets/lang`, the app's own catalogue location), flattens it the same way the app's own loader does, and installs it before awaiting the translator's load:

```dart
await MagicTest.loadTranslations('tr');
expect(trans('auth.login'), 'Giriş yap');
```

Point `directory` at a temp directory with a small fixture json to test one key in isolation without touching the app's real catalogue. Loading never requires a fallback-locale file on disk: the translator only warns and falls back to an empty map when the fallback catalogue fails to load.

## Writing Tests

### Basic Test Structure

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/testing.dart';

void main() {
  MagicTest.init();

  group('User', () {
    test('can create a user', () async {
      final user = User()
        ..fill({
          'name': 'John Doe',
          'email': 'john@example.com',
        });
      
      await user.save();
      
      expect(user.id, isNotNull);
      expect(user.name, equals('John Doe'));
    });

    test('validates email format', () {
      final validator = EmailValidator();
      
      expect(validator.passes('john@example.com'), isTrue);
      expect(validator.passes('invalid-email'), isFalse);
    });
  });
}
```

## Controller Testing

Test controllers by creating instances and calling methods:

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

void main() {
  late AuthController controller;
  
  setUp(() {
    controller = AuthController();
  });

  tearDown(() {
    controller.dispose();
  });

  group('AuthController', () {
    test('login sets loading state', () async {
      expect(controller.isLoading, isFalse);
      
      // Start login (will fail due to no mock, but tests state)
      controller.login({'email': 'test@test.com', 'password': 'password'});
      
      expect(controller.isLoading, isTrue);
    });

    test('clearErrors removes all validation errors', () {
      controller.setError('email', 'Invalid email');
      expect(controller.hasError('email'), isTrue);
      
      controller.clearErrors();
      expect(controller.hasError('email'), isFalse);
    });
  });
}
```

### Testing State Changes

```dart
test('setSuccess updates state correctly', () {
  final controller = TaskController();
  
  expect(controller.isLoading, isFalse);
  expect(controller.isSuccess, isFalse);
  
  controller.setLoading();
  expect(controller.isLoading, isTrue);
  
  controller.setSuccess([Task(name: 'Test')]);
  expect(controller.isSuccess, isTrue);
  expect(controller.data, isNotEmpty);
});
```

## Model Testing

Test models using factories for consistent data:

```dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  setUpAll(() async {
    await setupTestEnvironment();
    await Migrator().run([CreateUsersTable()]);
  });

  group('User Model', () {
    test('creates user with factory', () async {
      final users = await UserFactory().count(5).create();
      
      expect(users.length, equals(5));
      expect(users.first.id, isNotNull);
    });

    test('finds user by id', () async {
      final created = (await UserFactory().create()).first;
      final found = await User.find(created.id);
      
      expect(found, isNotNull);
      expect(found!.email, equals(created.email));
    });

    test('updates user attributes', () async {
      final user = (await UserFactory().create()).first;
      
      user.name = 'Updated Name';
      await user.save();
      
      final fresh = await User.find(user.id);
      expect(fresh!.name, equals('Updated Name'));
    });

    test('deletes user', () async {
      final user = (await UserFactory().create()).first;
      final id = user.id;
      
      await user.delete();
      
      final found = await User.find(id);
      expect(found, isNull);
    });
  });
}
```

## Mock HTTP Responses

Use mock HTTP responses for testing API interactions:

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements HttpClient {}

void main() {
  late MockHttpClient mockHttp;
  late AuthController controller;

  setUp(() {
    mockHttp = MockHttpClient();
    // Inject mock into Http facade
    Http.setTestClient(mockHttp);
    controller = AuthController();
  });

  test('login success redirects to dashboard', () async {
    // Arrange
    when(() => mockHttp.post('/login', data: any(named: 'data')))
        .thenAnswer((_) async => MagicResponse(
          statusCode: 200,
          body: {
            'token': 'test-token',
            'user': {'id': 1, 'name': 'John'},
          },
        ));

    // Act
    await controller.login({
      'email': 'john@example.com',
      'password': 'password',
    });

    // Assert
    expect(Auth.check(), isTrue);
  });

  test('login failure shows error', () async {
    when(() => mockHttp.post('/login', data: any(named: 'data')))
        .thenAnswer((_) async => MagicResponse(
          statusCode: 401,
          body: {'message': 'Invalid credentials'},
        ));

    await controller.login({
      'email': 'john@example.com',
      'password': 'wrong',
    });

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

## Running Tests

### Run All Tests

```bash
flutter test
```

### Run Specific Test File

```bash
flutter test test/unit/models/user_test.dart
```

### Run With Coverage

```bash
flutter test --coverage
```

### Test Output

```bash
flutter test --reporter=expanded
```

> [!TIP]
> Use the `:memory:` database driver for faster tests that don't persist to disk.
