Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I can't mock a SupabaseClient instance #714

Open
fueripe-desu opened this issue Nov 18, 2023 · 4 comments
Open

I can't mock a SupabaseClient instance #714

fueripe-desu opened this issue Nov 18, 2023 · 4 comments
Labels
bug Something isn't working

Comments

@fueripe-desu
Copy link

fueripe-desu commented Nov 18, 2023

Describe the bug
I'm trying to mock SupabaseClient using the mockito package and it keeps giving me this timeout exception: TimeoutException after 0:00:30.000000: Test timed out after 30 seconds. See https://pub.dev/packages/test#timeouts dart:isolate _RawReceivePort._handleMessage

To Reproduce
In order to reproduce this exception, you can use the following test:

@GenerateNiceMocks([
  MockSpec<SupabaseClient>(),
  MockSpec<SupabaseQueryBuilder>(),
  MockSpec<PostgrestFilterBuilder<List<Map<String, dynamic>>>>(),
  MockSpec<PostgrestResponse<List<Map<String, dynamic>>>>()
])
import 'sync_remote_datasource_test.mocks.dart';

class FakeDatabaseClient extends DatabaseClientInterface {
  FakeDatabaseClient({
    required this.supabaseClient,
  });
  final MockSupabaseClient supabaseClient;

  @override
  // TODO: implement local
  Isar get local => throw UnimplementedError();

  @override
  MockSupabaseClient get remote => supabaseClient;
}

void main() {
  late SyncRemoteDataSourceImpl syncRemoteDataSourceImpl;
  late FakeDatabaseClient mockDatabaseClient;

  setUp(() {
    mockDatabaseClient = FakeDatabaseClient(
      supabaseClient: MockSupabaseClient(),
    );
    syncRemoteDataSourceImpl =
        SyncRemoteDataSourceImpl(client: mockDatabaseClient);
  });

  test('Should return a list of languages from the remote database', () async {
    // arrange
    final fixtureMap = fixture('language_list_fixture.json');

    final mockQueryBuilder = MockSupabaseQueryBuilder();
    final parsedList = json.decode(fixtureMap) as List<dynamic>;

    final expectedResult = parsedList
        .map((dynamic item) =>
            Map<String, dynamic>.from(item as Map<String, dynamic>))
        .toList();

    final mockPostgrestFilterBuilder = MockPostgrestFilterBuilder();
    final mockPostgrestResponse = MockPostgrestResponse();

    // Mock the behavior of `from` method
    when(mockDatabaseClient.remote.from(any)).thenAnswer(
      (_) => mockQueryBuilder,
    );

    // Mock the behavior of `select` method to return
    // the PostgrestFilterBuilder instance
    when(mockQueryBuilder.select<List<Map<String, dynamic>>>(any))
        .thenAnswer((_) => mockPostgrestFilterBuilder);

    // Return your expected result when using the mocked
    // PostgrestFilterBuilder instance
    when(mockPostgrestFilterBuilder.execute())
        .thenAnswer((_) async => mockPostgrestResponse);

    // Mock the behavior of `data` getter on PostgrestResponse to return
    // your expected result
    when(mockPostgrestResponse.data).thenReturn(expectedResult);

    // act
    final result = await syncRemoteDataSourceImpl.fetchLanguages();

    // assert
    expect(result, expectedResult);
  });
}

The definition of the DatabaseClientInterface is the following:

abstract class DatabaseClientInterface {
  SupabaseClient get remote;
  Isar get local;
}

The fixture() function just reads a string synchronously from the fixtures directory:

String fixture(String name) => File('test/fixtures/$name').readAsStringSync();

The structure of the JSON file I'm trying to read (just for more context) is the following:

[
    {
        "uuid": "2094025c-85a2-43d9-b51d-decad64fd3b5",
        "created_at": "2023-11-10 19:03:42.601115+00",
        "user_id": "a37ab29c-7bed-4acc-ad9d-9ae7177d4da4",
        "value": "English"
    },
    {
        "uuid": "ea04054d-6757-4f74-9e72-19f1fb8b3c3a",
        "created_at": "2023-08-10 19:03:42.601115+00",
        "user_id": "a37ab29c-7bed-4acc-ad9d-9ae7177d4da4",
        "value": "Portuguese"
    },
    {
        "uuid": "004e0027-4bbe-4d28-97e4-6dc9acd9fd96",
        "created_at": "2023-04-10 19:03:42.601115+00",
        "user_id": "a37ab29c-7bed-4acc-ad9d-9ae7177d4da4",
        "value": "Japanese"
    }
]

And here is the SyncRemoteDatasourceImpl definition:

abstract class SyncRemoteDataSource {
  Future<bool> getSyncState();
  Future<List<Map<String, dynamic>>> fetchLanguages();
}

class SyncRemoteDataSourceImpl implements SyncRemoteDataSource {
  SyncRemoteDataSourceImpl({required this.client});
  final DatabaseClientInterface client;

  @override
  Future<List<Map<String, dynamic>>> fetchLanguages() async {
    final languages = await client.remote.from('languages').select('*');
    return languages as List<Map<String, dynamic>>;
  }

  @override
  Future<bool> getSyncState() {
    // TODO: implement getSyncState
    throw UnimplementedError();
  }
}

Expected behavior
The expected behavior would be for the test to pass without throwing a timeout exception.

Version:
On Linux/macOS

altime_client|functions_client"
└── supabase_flutter 1.10.25
    ├── supabase 1.11.11
    │   ├── functions_client 1.3.2
    │   ├── gotrue 1.12.6
    │   ├── postgrest 1.5.2
    │   ├── realtime_client 1.4.0
    │   ├── storage_client 1.5.4

Additional context
Since I'm very new to Supabase and everything, I don't really know if this is a bug or if it is my logic that is flawed, or if mocking the SupabaseClient is possible in the first place, but I have tried doing this in a lot of different ways and all of them just kept throwing me this timeout exception. Something I might want to mention is that, before doing this way, I started searching for more information about mocking the SupabaseClient and I came across this old Github issue from the supabase-dart package, where the user was trying to achieve something very similar to what I want, and an example of a mock client was proposed in the issue, I used it and I had to adapt it and remove some errors, but then it kept throwing me this timeout exception.

Here is the link of the issue I mentioned: supabase/supabase-dart#12

@fueripe-desu fueripe-desu added the bug Something isn't working label Nov 18, 2023
@Vinzent03
Copy link
Collaborator

Your code example is quite large so I don't know where exactly the issue happens. Additionally, I'm not really familiar with your mock package. I guess there is some cleanup needed for the isolate we use internally for json decoding.

@fueripe-desu
Copy link
Author

Your code example is quite large so I don't know where exactly the issue happens. Additionally, I'm not really familiar with your mock package. I guess there is some cleanup needed for the isolate we use internally for json decoding.

Yes, I shall admit that my code is a lot verbose, and this version I posted here had a little bug that I've later found out, because in the decorator used to generate mocks, this one:

@GenerateNiceMocks([
  MockSpec<SupabaseClient>(),
  MockSpec<SupabaseQueryBuilder>(),
  MockSpec<PostgrestFilterBuilder<List<Map<String, dynamic>>>>(),
  MockSpec<PostgrestResponse<List<Map<String, dynamic>>>>()
])

PostgrestFilterBuilder() and PostgrestResponse() were marked with the generic type List<Map<String, dynamic>>> but Supabase returns List<dynamic> instead, so I fixed it, but the bug still happens in the test.

Regarding the mockito package, it just implements a class getting rid of its required parameters and passing mock parameters to it, then you can simply mock the behavior of the class for example:

final mockQueryBuilder = MockSupabaseQueryBuilder();
final mockPostgrestFilterBuilder = MockPostgrestFilterBuilder();

when(mockQueryBuilder.select<List<Map<String, dynamic>>>(any))
        .thenAnswer((_) => mockPostgrestFilterBuilder);

In this line, both these mock classes were created by mockito, so they don't really need parameters you can just instantiate them, then this when().thenAnswer() method is just saying that when this method is called in the production code while running the test it will just return the value of thenAnswer() just to mock the behavior because it's not the real class.

My guess of what could be the actual problem could be that mockito doesn't really work that well with isolates or the isolate used for JSON decoding in the actual Supabase source code has some kind of problem.

The workaround I used for solving this problem was creating a FakeSupabaseClient class that copies the syntax of Supabase but it works totally offline without mocking the HTTP client.

@Vinzent03
Copy link
Collaborator

Thanks for the explanation and simplification. I guess the constructor of SupabaseClient is still called, which creates the isolate. Do you have any way to call .dispose() on that object?

@fueripe-desu
Copy link
Author

fueripe-desu commented Nov 30, 2023

I tried calling the .dispose() method like this to see if something would change:

test('Should return a list of languages from the remote database', () async {
    // arrange
    final fixtureMap = fixture('language_list_fixture.json');

    final mockQueryBuilder = MockSupabaseQueryBuilder();
    final parsedList = json.decode(fixtureMap) as List<dynamic>;

    final expectedResult = parsedList
        .map((dynamic item) =>
            Map<String, dynamic>.from(item as Map<String, dynamic>))
        .toList();

    final mockPostgrestFilterBuilder = MockPostgrestFilterBuilder();
    final mockPostgrestResponse = MockPostgrestResponse();

    // Mock the behavior of `from` method
    when(mockDatabaseClient.remote.from(any)).thenAnswer(
      (_) => mockQueryBuilder,
    );

    // Mock the behavior of `select` method to return
    // the PostgrestFilterBuilder instance
    when(mockQueryBuilder.select<List<dynamic>>(any, any))
        .thenAnswer((_) => mockPostgrestFilterBuilder);

    // Return your expected result when using the mocked
    // PostgrestFilterBuilder instance
    when(mockPostgrestFilterBuilder.execute())
        .thenAnswer((_) async => mockPostgrestResponse);

    // Mock the behavior of `data` getter on PostgrestResponse to return
    // your expected result
    when(mockPostgrestResponse.data).thenReturn(expectedResult);

    // act
    final result = await syncRemoteDataSourceImpl.fetchLanguages();

    // assert
    expect(result, expectedResult);

    await mockDatabaseClient.supabaseClient.dispose();
  });

but mockito overrides the .dispose() method, so it doesn't really have any effect, and I can't access the isolate because it is a private property of SupabaseClient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants