tekartik / sembast.dart

Simple io database
BSD 2-Clause "Simplified" License
780 stars 64 forks source link

[Question] Best way to test `RecordRef.onSnapshot`? #300

Closed mrverdant13 closed 2 years ago

mrverdant13 commented 2 years ago

What is the best way to test RecordRef.onSnapshot?

On regular Future-based methods, the info provided in the docs is more than enough for tests.

Nevertheless, for Stream-based methods, this is not the case.

Let's say we have a watcher method as follows:

Stream<int> watchValue() async* {
  yield await 0;
  yield* record.onSnapshot(db);
}

I wonder what is the best approach to test a method like this one.

So far, I only think of mocking the StoreRef that returns a mock record and, at the same time, this record has a stubbed onSnapshot method to return a fake Stream of values. However, this is cumbersome, so I wanted to ask for advice here.

Thanks for the awesome package!

mrverdant13 commented 2 years ago

Actually, I just noticed that onSnapshot is an extension method. Thus, it cannot be stubbed. 😔

alextekartik commented 2 years ago

Yes testing streams is not always convenient. I'm not an expert in using expectLater, emitsInOrder but the rxdart package has some good examples.

Regarding mocking, well, the purpose of inMemory database factory is to provide a testable context (i.e. you can use it to mock whatever you want since you can simply write to the database to add test data. Indeed onSnapshot are extensions, basically record and store refs are just definitions, but these extensions ends up calling sembast and the whole abstraction is around the factory concept (sorry I come from the java world). Maybe it is a bad choice (and code completion is not always as good for extension than for methods).

Here is a simple onSnapshot test:

import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_memory.dart';
import 'package:test/test.dart';

void main() {
  // This is needed for flutter tests
  disableSembastCooperator();

  // Using in memory factory
  var factory = databaseFactoryMemory;

  test('onSnapshot', () async {
    late Database db;
    // Key is an int, value is an int
    var store = StoreRef<int, int>.main();
    // Key is 1
    var record = store.record(1);

    // onSnapshot values
    Stream<int?> watchValue() async* {
      yield* record.onSnapshot(db).map((record) => record?.value);
    }

    // Create a blank in memory database
    db = await factory.openDatabase(sembastInMemoryDatabasePath);

    // Test stream, expect value null, 1, 2, null (not existing, value 1 created, value 2 updated, deleted);
    var future = watchValue().take(4).toList();

    // add
    await record.put(db, 1);
    // update
    await record.put(db, 2);
    // delete
    await record.delete(db);

    // Test stream result.
    expect(await future, [null, 1, 2, null]);

    await db.close();
  });
}

Sometimes I also manually listen and cancel subscription for finer testing. This repository has some tests (https://github.com/tekartik/sembast.dart/blob/master/sembast_test/lib/listener_test.dart). Some tests are very old (I think even some were made before await was available) but I still maintain them.

However I would be happy if someone has a better ways to test this.

mrverdant13 commented 2 years ago

Fair enough. Thanks for the detailed answer!

I ended up listening to the records on my tests and I worked like a charm. As you said, it is more suitable for more controlled testing.

PS: The in-memory DB would be considered a fake instead of a mock. Thanks again for the awesome package! 🤜🏼✨🤛🏼