flutter
/

Testing GetX Controllers & Services: Unit Tests & Integration

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Testing GetX Controllers & Services: Unit Tests & Integration

Introduction to Testing GetX

Testing is essential for building reliable Flutter apps. GetX makes it easy to test your controllers and services because they are plain Dart classes with dependency injection. With a few simple techniques, you can write unit tests that verify state changes, business logic, and side effects. This guide covers how to test GetX controllers, use mocking, and manage the dependency injection container during tests.

Setting Up the Test Environment

Add the required dependencies to your pubspec.yaml:

YAMLRead-only
1
dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.0
  build_runner: ^2.4.0

Create a test file in the test/ folder, e.g., test/controllers/login_controller_test.dart. Use setUp and tearDown to reset GetX before each test.

DARTRead-only
1
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:your_app/controllers/login_controller.dart';

void main() {
  setUp(() {
    Get.reset(); // Clears all registered dependencies
  });

  tearDown(() {
    Get.reset();
  });

  // tests go here
}

Testing a Basic Controller

Consider a simple counter controller:

DARTRead-only
1
class CounterController extends GetxController {
  var count = 0.obs;

  void increment() => count++;
  void decrement() => count--;
}

Test it by creating an instance and verifying state changes:

DARTRead-only
1
test('CounterController initial value is 0', () {
  final controller = CounterController();
  expect(controller.count.value, 0);
});

test('increment increases count', () {
  final controller = CounterController();
  controller.increment();
  expect(controller.count.value, 1);
});

test('decrement decreases count', () {
  final controller = CounterController();
  controller.decrement();
  expect(controller.count.value, -1);
});

Testing Controllers with Dependencies

When a controller depends on a service, you should mock that service. Use Mockito to generate mocks.

DARTRead-only
1
// Define an abstract repository
abstract class IUserRepository {
  Future<String> login(String email, String password);
}

// Create a mock with Mockito
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

@GenerateMocks([IUserRepository])
void main() { ... }
DARTRead-only
1
// Controller that uses the repository
class LoginController extends GetxController {
  final IUserRepository repository;
  LoginController(this.repository);

  var isLoading = false.obs;
  var error = ''.obs;

  Future<bool> login(String email, String password) async {
    isLoading.value = true;
    error.value = '';
    try {
      await repository.login(email, password);
      return true;
    } catch (e) {
      error.value = e.toString();
      return false;
    } finally {
      isLoading.value = false;
    }
  }
}

Now write a test using the mock:

DARTRead-only
1
import 'package:mockito/mockito.dart';
import 'login_controller_test.mocks.dart';

void main() {
  late MockIUserRepository mockRepo;
  late LoginController controller;

  setUp(() {
    Get.reset();
    mockRepo = MockIUserRepository();
    controller = LoginController(mockRepo);
  });

  test('login success updates state', () async {
    when(mockRepo.login('test@test.com', '123'))
        .thenAnswer((_) async => 'token');

    final result = await controller.login('test@test.com', '123');

    expect(result, true);
    expect(controller.isLoading.value, false);
    expect(controller.error.value, '');
    verify(mockRepo.login('test@test.com', '123')).called(1);
  });

  test('login failure sets error', () async {
    when(mockRepo.login('test@test.com', 'wrong'))
        .thenThrow(Exception('Invalid credentials'));

    final result = await controller.login('test@test.com', 'wrong');

    expect(result, false);
    expect(controller.isLoading.value, false);
    expect(controller.error.value, 'Exception: Invalid credentials');
  });
}

Testing Reactive State with Get.reset

When your controller uses Get.put or Get.find, you need to manage the GetX dependency container. Use Get.reset() before each test to clear registrations. Then you can manually inject mocks with Get.put.

DARTRead-only
1
test('controller uses Get.find', () {
  Get.lazyPut<MyService>(() => MockMyService());
  final controller = Get.put(MyController());
  // test logic
});

Testing Workers

Workers can be tested by changing the reactive variable and verifying the side effect. You may need to use Future.microtask or pump to let the worker execute.

DARTRead-only
1
class WorkerController extends GetxController {
  var query = ''.obs;
  var searchCalled = false.obs;

  @override
  void onInit() {
    super.onInit();
    debounce(query, (_) => searchCalled.value = true, time: Duration(milliseconds: 300));
  }
}

test('debounce worker triggers after delay', () async {
  final controller = WorkerController();
  controller.query.value = 'hello';

  // Worker hasn't triggered yet
  expect(controller.searchCalled.value, false);

  // Wait for debounce period
  await Future.delayed(Duration(milliseconds: 350));

  expect(controller.searchCalled.value, true);
});

Testing GetxService

Services are tested similarly to controllers, but they are usually permanent. In tests, you can instantiate them directly or use Get.put.

DARTRead-only
1
class MyService extends GetxService {
  int getValue() => 42;
}

test('MyService returns correct value', () {
  final service = MyService();
  expect(service.getValue(), 42);
});

Integration Testing with GetX

For widget tests that involve GetX widgets like Obx or GetBuilder, you can use GetMaterialApp as the test widget. Ensure you register controllers in the test environment, for example via bindings.

DARTRead-only
1
testWidgets('CounterPage shows initial count', (tester) async {
  await tester.pumpWidget(
    GetMaterialApp(
      home: CounterPage(),
      initialBinding: CounterBinding(),
    ),
  );

  expect(find.text('0'), findsOneWidget);
  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();
  expect(find.text('1'), findsOneWidget);
});

Best Practices

  • Reset GetX before each test – Use setUp(() => Get.reset()) to avoid state leakage.
  • Mock dependencies – Use Mockito or manual mocks to isolate the controller under test.
  • Test reactive variables directly – They are just Dart objects; you can read their .value.
  • Avoid testing implementation details – Focus on public methods and state changes.
  • Use Future.delayed for worker tests – Account for debounce/interval durations.
  • Keep tests fast – Avoid real network calls; mock them.

Common Mistakes

  • ❌ Not resetting GetX – Previous registrations can cause tests to interfere. ✅ Call Get.reset() in setUp.
  • ❌ Testing reactive state with == on .value – Works, but ensure you're comparing values, not references.
  • ❌ Using Get.find without registration – Throws error. Register mocks first.
  • ❌ Forgetting to await async operations – Use await or pump to let futures complete.
  • ❌ Testing workers with hardcoded delays – Use realistic but small delays, or use pumpAndSettle in widget tests.

FAQ

  • Q: Do I need to mock GetX itself?
    A: No, GetX is a framework. You just mock your own dependencies.
  • Q: How do I test a controller that uses Get.parameters?
    A: You can either pass parameters via the route in a widget test, or extract the logic into a method that receives the parameter.
  • Q: Can I test the onInit lifecycle?
    A: Yes. When you instantiate a controller, onInit runs automatically. You can test side effects after instantiation.
  • Q: How to test Get.snackbar calls?
    A: In unit tests, you can mock Get with a custom implementation. For widget tests, you can verify the snackbar appears.
  • Q: Is it possible to test GetX navigation?
    A: Yes, in widget tests you can tap buttons that call Get.to and verify that the new route is built.

Conclusion

Testing GetX controllers and services is straightforward once you understand how to manage the DI container and mock dependencies. With Get.reset() and proper mocks, you can write reliable unit tests that cover your business logic. Combine these with widget tests for full confidence in your Flutter app.

Test Your Knowledge

Q1
of 3

What method should you call in setUp to clear all registered dependencies?

A
Get.clean()
B
Get.clear()
C
Get.reset()
D
Get.deleteAll()
Q2
of 3

How can you mock a repository for a controller test?

A
Use the real implementation
B
Create a mock using Mockito and inject it
C
Use Get.find to find the real one
D
Hardcode values in the controller
Q3
of 3

When testing a worker with debounce, how do you simulate the delay?

A
Call pump()
B
Use Future.delayed
C
Use fakeAsync
D
Both B and C

Previous

getx state mixin

Next

getx theming

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.