How to Unit Test Flutter with Supabase Abstraction

Summary

A developer attempting to implement unit testing in a Flutter application encountered a significant roadblock: the inability to mock the Supabase client. Instead of writing isolated tests for business logic, the developer found themselves struggling with the complexity of the Supabase SDK, leading to frustration and an inability to progress with a robust testing strategy. This is a classic case of tight coupling between application logic and a third-party infrastructure provider.

Root Cause

The core issue is not a lack of information on Google, but rather an architectural flaw in how the Supabase client is being consumed.

  • Direct Dependency Injection of Concrete Classes: The application likely injects the SupabaseClient directly into services or controllers.
  • Complexity of the SDK: The Supabase client is a massive, complex object with deeply nested method chains (e.g., supabase.from('table').select().eq(...)). Mocking these nested calls using standard mocking libraries like mockito or mocktail becomes an exponentially difficult task.
  • Lack of Abstraction: There is no Interface Layer or Repository Pattern acting as a buffer between the domain logic and the external SDK.

Why This Happens in Real Systems

In rapid development cycles, engineers often prioritize feature velocity over testability.

  • The “Golden Path” Bias: During initial development, engineers follow the “golden path” provided by documentation, which often shows direct usage of the client.
  • Oversight of Testability: Developers often treat third-party SDKs as simple utilities (like math or string libraries) rather than external dependencies that require boundaries.
  • Dependency Bloat: As the application grows, the direct usage of a heavy client like Supabase spreads across the entire codebase, making it impossible to isolate components for testing.

Real-World Impact

  • Brittle Tests: Even if a mock is created, any minor change in the Supabase SDK version can break hundreds of tests.
  • Slow Test Suites: If developers resort to “integration testing” (hitting the actual Supabase backend) because they can’t mock the client, the test suite becomes slow, flaky, and requires internet connectivity.
  • Development Stagnation: Teams spend more time fighting the test harness than writing actual features.

Example or Code

abstract class UserRepository {
  Future<Map?> getUser(String id);
}

class SupabaseUserRepository implements UserRepository {
  final SupabaseClient _supabase;

  SupabaseUserRepository(this._supabase);

  @override
  Future<Map?> getUser(String id) async {
    final data = await _supabase
        .from('profiles')
        .select()
        .eq('id', id)
        .single();
    return data;
  }
}

class MockUserRepository extends Mock implements UserRepository {}

How Senior Engineers Fix It

Senior engineers solve this by applying the Dependency Inversion Principle (DIP). They never mock the SDK; they mock the interface they created to wrap the SDK.

  • Implement the Repository Pattern: Create an abstract class that defines the operations your app needs (e.g., fetchUser, savePost).
  • Create a Concrete Implementation: Create a SupabaseRepository that implements that interface and contains the actual Supabase logic.
  • Inject the Interface, Not the SDK: Your business logic (Bloc, Provider, or Controller) should only know about UserRepository, not SupabaseClient.
  • Mock the Repository: In unit tests, you use a mock implementation of your UserRepository. This makes tests predictable, fast, and incredibly simple to write because you are only mocking a single method call rather than a complex chain of SDK calls.

Why Juniors Miss It

  • Focus on “How to Mock” vs. “How to Design”: Juniors search for “how to mock X library,” while seniors ask “how can I design my code so I don’t have to mock X library?”
  • Misunderstanding Unit Testing: Juniors often mistake unit testing for “testing the library I am using,” whereas the goal of a unit test is to test your own code’s reaction to specific data inputs and outputs.
  • Underestimating Abstraction: The concept of an abstract interface seems like “extra work” or “boilerplate” to a junior, but seniors recognize it as essential insurance against architectural rot.

Leave a Comment