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

GetStorage strange behaviour in unit tests #37

Closed
yulkin2002 opened this issue Dec 8, 2020 · 13 comments
Closed

GetStorage strange behaviour in unit tests #37

yulkin2002 opened this issue Dec 8, 2020 · 13 comments

Comments

@yulkin2002
Copy link

yulkin2002 commented Dec 8, 2020

I am writing unit tests for my implementation of GetStorage shared preferences alongside GetX and it would be so helpful to see how to properly use both GetStorage and GetX controllers in unit tests.
I see some very inconsistent GetStorage behaviour in my unit tests - sometimes the data gets updated, other times not and most of the time the data persists until another test is run, even though I initialize the class from scratch every time .

The unit test does not pick up the correct values inside the variables. For example, both userData.firstName.val, errorController.contactInfoMissing.value inside the test even though I confirmed they are both being set with correct values in the code. This leads me to believe that I may doing something wrong inside the unit test, but there is no documentation on how to do it properly.

P.S. StoredUserData works fine in my app, but for some reason not in the unit test.

Also, I wasn't able to initialize the ErrorController inside the setUp of the unit test. StoredUserData is not finding it in this case, therefore I had to initialize it in the test itself - not sure if proper.

Unit test code

class MockDocumentSnapshot extends Mock implements DocumentSnapshot {
  Map<String, dynamic> mockData;
  MockDocumentSnapshot(this.mockData);

  @override
  Map<String, dynamic> data() {
    return mockData;
  }
  @override
  bool get exists => true;
}

class MockFirestoreService extends Mock implements FirestoreService {
  final MockDocumentSnapshot documentSnapshot;

  MockFirestoreService(this.documentSnapshot);
  @override
  Future<MockDocumentSnapshot> readData(String collectionName, String documentPath) {
    return Future.delayed(Duration(milliseconds: 0)).then((value) => documentSnapshot);
  }
}

void main() {
  setupCloudFirestoreMocks();

  setUpAll(() async {
    TestWidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
 await GetStorage.init();

  });
 test(
        'update() method test: blank data, lastUpdatedDate is today and force update is true - update should run and contactInfoMissing value should be set to true',
        () {
      ErrorController errorController = Get.put(ErrorController());
      const Map<String, dynamic> blankUserAccountData = {};
      MockDocumentSnapshot mockDocumentSnapshot = MockDocumentSnapshot(blankUserAccountData);

      MockFirestoreService mockFirestoreService = MockFirestoreService(mockDocumentSnapshot);
      final StoredUserData userData = StoredUserData(firestoreService: mockFirestoreService);
await userData.erase();
      String today = DateTime.now().toString();
      userData.lastUpdated.val = today;
      // userData.erase();
      userData.update(true, 'email', 'UID');
      expect(userData.firstName.val, '');
      expect(userData.lastName.val, '');
      expect(userData.aptNumber.val, '');
      expect(userData.streetNumber.val, '');
      expect(userData.streetName.val, '');
      expect(userData.city.val, '');
      expect(userData.loginEmail.val, '');
      expect(userData.contactEmail.val, '');
      expect(userData.phone.val, '');
      expect(userData.visibility.val, {});
      expect(userData.languages.val, []);
      expect(userData.lastUpdated.val, DateTime.utc(2020, DateTime.november, 11).toString());

      expect(errorController.contactInfoMissing.value, true);

      Get.delete<ErrorController>();
    });
}

Code being tested:

class StoredUserData {
  ErrorController _errorController = Get.find<ErrorController>();
  FirestoreService _firestoreService = FirestoreService();
 
 StoredUserData({FirestoreService firestoreService}) : this.firestoreService = firestoreService ?? FirestoreService();
  static final _userDataBox = () => GetStorage('UserData');

  final firstName = ReadWriteValue(kdbFieldFirstName, '', _userDataBox);
  final lastName = ReadWriteValue(kdbFieldLastName, '', _userDataBox);
  final aptNumber = ReadWriteValue(kdbFieldAptNumber, '', _userDataBox);
  final streetNumber = ReadWriteValue(kdbFieldStreetNum, '', _userDataBox);
  final streetName = ReadWriteValue(kdbFieldStreetName, '', _userDataBox);
  final city = ReadWriteValue(kdbFieldCity, '', _userDataBox);
  final loginEmail = ReadWriteValue('loginEmail', '', _userDataBox);
  final contactEmail = ReadWriteValue(kdbFieldEmail, '', _userDataBox);
  final phone = ReadWriteValue(kdbFieldPhone, '', _userDataBox);
  final visibility = ReadWriteValue(kdbFieldVisibility, {}, _userDataBox);
  final languages = ReadWriteValue(kdbFieldLanguages, [], _userDataBox);
  final lastUpdated = ReadWriteValue('lastUpdated', DateTime.utc(2020, DateTime.november, 11).toString(), _userDataBox);

  Future<Map<String, dynamic>> getFreshDataFromDB(String userID) async {
    Map<String, dynamic> _userSnapshotData;
    
    await _firestoreService.readData(kdbCollUserAccounts, userID).then((docSnapshot) => _userSnapshotData = docSnapshot.data()).catchError((error) {
     
      _errorController.errorMessage.value = error.toString();
    });
   
    return _userSnapshotData;
  }

  void update(bool forceUpdate, String userEmail, String userID) async {
    
    
    final DateTime today = DateTime.now();
    final DateTime _lastUpdated = DateTime.parse(lastUpdated.val);
   
    if (_userDataBox.isNullOrBlank || _lastUpdated.difference(today).inDays < -1 || forceUpdate) {
      print('GetX Storage Called: date test is true');
      try {
       
        Map<String, dynamic> _userSnapshotData = await getFreshDataFromDB(userID);
        print('GetX Storage Called: usersnapshotdata after getFreshDataCalled - $_userSnapshotData');
        if (_userSnapshotData != null) {
          lastUpdated.val = DateTime.now().toString();
          Map _address = _userSnapshotData[kdbFieldAddress];
          Map _contactInfo = _userSnapshotData[kdbFieldContactInfo];
          print('GetX Storage Called: first name before update - ${firstName.val}; DateTime: ${DateTime.now().toString()}');
          print('GetX Storage Called: last name before update - ${lastName.val}; DateTime: ${DateTime.now().toString()}');
          firstName.val = _userSnapshotData[kdbFieldFirstName].toString();
          lastName.val = _userSnapshotData[kdbFieldLastName].toString();
         
          if (_address.isNullOrBlank) {
            _errorController.contactInfoMissing.value = true;
            lastUpdated.val = DateTime.utc(2020, DateTime.november, 11).toString();
           } else {
            _errorController.contactInfoMissing.value = false;
            aptNumber.val = !_address.isNullOrBlank ? _address[kdbFieldAptNumber] : '';
            streetNumber.val = !_address.isNullOrBlank ? _address[kdbFieldStreetNum] : '';
            streetName.val = !_address.isNullOrBlank ? _address[kdbFieldStreetName] : '';
            city.val = !_address.isNullOrBlank ? _address[kdbFieldCity] : '';
          }
          loginEmail.val = userEmail;
         if (!_contactInfo.isNullOrBlank) {
            String _email = _contactInfo[kdbFieldEmail];

            contactEmail.val = _email.isNullOrBlank ? userEmail : _email;
            phone.val = _contactInfo[kdbFieldPhone];
          } else {
            contactEmail.val = userEmail;
            phone.val = '';
          }
             visibility.val = _userSnapshotData[kdbFieldVisibility] ?? {};
          languages.val = _userSnapshotData[kdbFieldLanguages] ?? [];
        }
      } catch (e) {
        print('GetX Storage Called: error caught - ${e.toString()}');
        _errorController.errorMessage.value = e.toString();
      }
    } else {
      print('GetX Storage Called: UserData was updated less than one day ago');
    }
 void erase() async {
    await GetStorage('UserData').erase();
    print('GetStorage.erase() called');
  }
   
  }

@yulkin2002
Copy link
Author

yulkin2002 commented Dec 8, 2020

I have added another test and now it looks like the StorageData updates from the second test made their way into the first test and both are failing

test code

  test('update() method test: full account data, lastUpdatedDate is today and force update is true - all data should be updated', () {
      ErrorController errorController = Get.put(ErrorController());

      const Map<String, dynamic> userAccountData = {
        'lastName': 'Samovich',
        'firstName': 'Sam',
        'address': {'streetName': 'some new st', 'city': 'Toronto', 'aptNum': '2', 'streetNum': '3'},
        'contactInfo': {'phone': '4160000000', 'email': 'email@yahoo.com'},
        'languages': ['English', 'Russian', 'Ukrainian'],
        'visibility': {'phone': 'true', 'aptNum': 'true', 'streetNum': 'true', 'email': 'true'}
      };
      MockDocumentSnapshot mockDocumentSnapshot = MockDocumentSnapshot(userAccountData);

      MockFirestoreService mockFirestoreService = MockFirestoreService(mockDocumentSnapshot);
      final StoredUserData userData = StoredUserData(firestoreService: mockFirestoreService);
await userData.erase();
      String today = DateTime.now().toString();
      userData.lastUpdated.val = today;
      // userData.erase();
      userData.update(true, 'email', 'UID');
      expect(userData.firstName.val, 'Sam');
      expect(userData.lastName.val, 'Samovich');
      expect(userData.aptNumber.val, '2');
      expect(userData.streetNumber.val, '3');
      expect(userData.streetName.val, 'some new st');
      expect(userData.city.val, 'Toronto');
      expect(userData.loginEmail.val, 'email');
      expect(userData.contactEmail.val, 'email@yahoo.com');
      expect(userData.phone.val, '4160000000');
      expect(userData.visibility.val, {'phone': 'true', 'aptNum': 'true', 'streetNum': 'true', 'email': 'true'});
      expect(userData.languages.val, ['English', 'Russian', 'Ukrainian']);
      expect(userData.lastUpdated.val, today);

      expect(errorController.contactInfoMissing.value, false);

      Get.delete<ErrorController>();
    });

I tried erasing the storage before I run the update or at the end of the test, but it throws an exception in both cases:


GetStorage.erase() called
dart:io                                          FileSystemEntity.deleteSync
package:get_storage/src/storage/io.dart 21:18    StorageImpl.clear
===== asynchronous gap ===========================
dart:async                                       _asyncErrorWrapperHelper
package:nearby/getStorage/user_data.dart 119:28  StoredUserData.erase
test\unit_tests_GetStorage.dart 89:16            main.<fn>.<fn>

FileSystemException: Cannot delete file, path = 'C:\...\Documents/UserData.gs' (OS Error: The process cannot access the file because it is being used by another process.
, errno = 32)


@yulkin2002 yulkin2002 changed the title Unit test examples needed for GetStorage + GetX GetStorage strange behaviour in unit tests Dec 9, 2020
@yulkin2002
Copy link
Author

@jonataslaw can you take a look at this issue?

@jonataslaw
Copy link
Owner

Initially, you need to mock the storage (look at the testing example in the repository).
Second you must configure for the storage to point to the current folder.

Your code should look something like this:

  const channel = MethodChannel('plugins.flutter.io/path_provider');
  void setUpMockChannels(MethodChannel channel) {
    channel.setMockMethodCallHandler((MethodCall methodCall) async {
      if (methodCall.method == 'getApplicationDocumentsDirectory') {
        return '.';
      }
    });
  }

  setUpAll(() async {
    setUpMockChannels(channel);

@yulkin2002
Copy link
Author

yulkin2002 commented Dec 11, 2020

@jonataslaw when you say mock the storage per testing example, do you mean this?

 setUp(() async {
    await GetStorage.init();
    g = GetStorage();
    await g.erase();
  });  

I am using a shared preferences implementation of GetStorage, so the storage is a private member of StoredUserData class. Are you saying I need to expose it? or is the below sufficient:

  final StoredUserData userData = StoredUserData();
      await userData.erase();

I've added the storage configuration code you posted above, but still getting the errors:

FileSystemException: Cannot delete file, path = 'C:...\Documents/UserData.gs' (OS Error: The process cannot access the file because it is being used by another process.
, errno = 32)

@jonataslaw
Copy link
Owner

I inserted above exactly the missing code snippet in your application, just copy and paste.

@yulkin2002
Copy link
Author

I inserted above exactly the missing code snippet in your application, just copy and paste.

I did, still same problem

@jonataslaw
Copy link
Owner

I inserted above exactly the missing code snippet in your application, just copy and paste.

I did, still same problem

Impossible give "'C:...\Documents/UserData.gs'" error, since this path is not be used with example above

@yulkin2002
Copy link
Author

yulkin2002 commented Dec 11, 2020

I did flutter clean, but still getting this error. below is my updated code with your snippet.
I also tried deleting the files at that path, but I see them created again after I run the tests

Click To Expand


class MockDocumentSnapshot extends Mock implements DocumentSnapshot {
  Map<String, dynamic> mockData;
  MockDocumentSnapshot(this.mockData);

  @override
  Map<String, dynamic> data() {
    return mockData;
  }

  @override
  bool get exists => true;
}

class MockFirestoreService extends Mock implements FirestoreService {
  final MockDocumentSnapshot documentSnapshot;

  MockFirestoreService(this.documentSnapshot);
  @override
  Future<MockDocumentSnapshot> readData(String collectionName, String documentPath) {
    return Future.delayed(Duration(milliseconds: 0)).then((value) => documentSnapshot);
  }
}

void main() {
  setupCloudFirestoreMocks();

  const channel = MethodChannel('plugins.flutter.io/path_provider');
  void setUpMockChannels(MethodChannel channel) {
    channel.setMockMethodCallHandler((MethodCall methodCall) async {
      if (methodCall.method == 'getApplicationDocumentsDirectory') {
        return '.';
      }
    });
  }

  setUpAll(() async {
    TestWidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    await GetStorage.init();

    setUpMockChannels(channel);
  });

  tearDown(() {
    Get.delete<ErrorController>();
  });
  //
  group('get_storage: StoredUserData class tests', () {
    test('Constructor test: all values should start empty and starting lastUpdatedDate should be Nov 11, 2020', () {
      ErrorController _errorController = Get.put(ErrorController());
      final StoredUserData userData = StoredUserData();
      expect(userData.firstName.val, '');
      expect(userData.lastName.val, '');
      expect(userData.aptNumber.val, '');
      expect(userData.streetNumber.val, '');
      expect(userData.streetName.val, '');
      expect(userData.city.val, '');
      expect(userData.loginEmail.val, '');
      expect(userData.contactEmail.val, '');
      expect(userData.phone.val, '');
      expect(userData.visibility.val, {});
      expect(userData.languages.val, []);
      expect(userData.lastUpdated.val, DateTime.utc(2020, DateTime.november, 11).toString());
      Get.delete<ErrorController>();
    });

    test('update() method test: if lastUpdatedDate is today and force update is false - update should not run', () {
      ErrorController _errorController = Get.put(ErrorController());
      final StoredUserData userData = StoredUserData();
      userData.erase();
      String today = DateTime.now().toString();
      userData.lastUpdated.val = today;
      userData.update(false, 'test', 'test');

      expect(userData.firstName.val, '');
      expect(userData.lastName.val, '');
      expect(userData.aptNumber.val, '');
      expect(userData.streetNumber.val, '');
      expect(userData.streetName.val, '');
      expect(userData.city.val, '');
      expect(userData.loginEmail.val, '');
      expect(userData.contactEmail.val, '');
      expect(userData.phone.val, '');
      expect(userData.visibility.val, {});
      expect(userData.languages.val, []);
      expect(userData.lastUpdated.val, today);
      Get.delete<ErrorController>();
   
    });

test(
        'update() method test: blank data, lastUpdatedDate is today and force update is true - update should run and contactInfoMissing value should be set to true',
        () {
      ErrorController errorController = Get.put(ErrorController());
      const Map<String, dynamic> blankUserAccountData = {};
      MockDocumentSnapshot mockDocumentSnapshot = MockDocumentSnapshot(blankUserAccountData);

      MockFirestoreService mockFirestoreService = MockFirestoreService(mockDocumentSnapshot);
      final StoredUserData _userData = StoredUserData(firestoreService: mockFirestoreService);
      _userData.erase();
      String today = DateTime.now().toString();
      _userData.lastUpdated.val = today;

      _userData.update(true, 'email', 'UID');
      expect(_userData.firstName.val, '');
      expect(_userData.lastName.val, '');
      expect(_userData.aptNumber.val, '');
      expect(_userData.streetNumber.val, '');
      expect(_userData.streetName.val, '');
      expect(_userData.city.val, '');
      expect(_userData.loginEmail.val, 'email');
      expect(_userData.contactEmail.val, '');
      expect(_userData.phone.val, '');
      expect(_userData.visibility.val, {});
      expect(_userData.languages.val, []);
  
      Get.delete<ErrorController>();
    
    });

    test('update() method test: full account data, lastUpdatedDate is today and force update is true - all data should be updated', () {
      ErrorController errorController = Get.put(ErrorController());

      const Map<String, dynamic> userAccountData = {
        'lastName': 'Samovich',
        'firstName': 'Sam',
        'address': {'streetName': 'some new st', 'city': 'Toronto', 'aptNum': '2', 'streetNum': '3'},
        'contactInfo': {'phone': '4160000000', 'email': 'email@yahoo.com'},
        'languages': ['English', 'Russian', 'Ukrainian'],
        'visibility': {'phone': 'true', 'aptNum': 'true', 'streetNum': 'true', 'email': 'true'}
      };
      MockDocumentSnapshot mockDocumentSnapshot = MockDocumentSnapshot(userAccountData);

      MockFirestoreService mockFirestoreService = MockFirestoreService(mockDocumentSnapshot);
      final StoredUserData userData = StoredUserData(firestoreService: mockFirestoreService);
      userData.erase();
      String today = DateTime.now().toString();
      userData.lastUpdated.val = today;
   
      userData.update(true, 'email', 'UID');
      expect(userData.firstName.val, 'Sam');
      expect(userData.lastName.val, 'Samovich');
      expect(userData.aptNumber.val, '2');
      expect(userData.streetNumber.val, '3');
      expect(userData.streetName.val, 'some new st');
      expect(userData.city.val, 'Toronto');
      expect(userData.loginEmail.val, 'email');
      expect(userData.contactEmail.val, 'email@yahoo.com');
      expect(userData.phone.val, '4160000000');
      expect(userData.visibility.val, {'phone': 'true', 'aptNum': 'true', 'streetNum': 'true', 'email': 'true'});
      expect(userData.languages.val, ['English', 'Russian', 'Ukrainian']);
      expect(userData.lastUpdated.val, today);

      expect(errorController.contactInfoMissing.value, false);

      Get.delete<ErrorController>();
    });
  });
}

@yulkin2002
Copy link
Author

@jonataslaw , did not get a response from you on the above, please let me know

@yulkin2002
Copy link
Author

@jonataslaw I like your plugins, you did a great job creating them, but I unfortunately I cannot keep using them if they are not testable. I understand you are supporting them by yourself, but 16 days no response seems a little excessive

@ernesto
Copy link

ernesto commented Jul 19, 2021

I think this issue can be closed, because when mock the storage with the right way like the repository example, the tests works.

@yulkin2002
Copy link
Author

I never was able to get it to work as per my comments above and eventually had to stop using the plugin. It's up to you to close it or not

@alirezat66
Copy link

I never was able to get it to work as per my comments above and eventually had to stop using the plugin. It's up to you to close it or not

Exactly, even when I copy exactly the test of the package in my test I even get the same error, so Unfortunately I should say it is not testable at all.
MissingPluginException(No implementation found for method getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider_macos)
package:get_storage/src/storage_impl.dart 47:7 GetStorage._init

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants