Testing SharedPreferences plugin

933 views
Skip to first unread message

Nicholas Manning

unread,
Dec 17, 2017, 3:04:30 AM12/17/17
to Flutter Dev
Hello, I'm trying to use the SharedPreferences plugin here but am having some issues figuring out how to test it.  

It seems like naively using SharedPreferences in my tests won't work, since a simulator needs to run to offer the ability to write to its SharedPreferences store.

Here's my class I'm trying to test for example:

import 'dart:core';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';

class Auth {
 
static final String SID_KEY = 'sid';

 
static Future<String> sessionID() async {
   
SharedPreferences prefs = await SharedPreferences.getInstance();
   
return prefs.getString(SID_KEY);
 
}

 
static setSessionID(String sid) async {
   
SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs
.setString(SID_KEY, sid);
 
}

 
static clearSessionID() async {
   
SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs
.remove(SID_KEY);
 
}
}

So what's the best way to test this?  By (a) mocking or (b) by actually running the simulator?  

With the former, (a), I can't figure out how to mock properly, as the documentation isn't quite clear. 

Example:

const MethodChannel('plugins.flutter.io/shared_preferences')
 
.setMockMethodCallHandler((MethodCall methodCall)
async {
   
if (methodCall.method == 'getAll') {
     
return <String, dynamic>{}; // set initial values here if desired
   
}
   
return null;
 
});


With the latter, (b) (integration testing in Flutter), it seems pretty complex for just this simple test.

Thanks in advance.

Mikkel Ravn

unread,
Dec 17, 2017, 5:13:39 AM12/17/17
to Nicholas Manning, Flutter Dev
If you are testing just the Auth class, it seems to me you should be mocking SharedPreferences (rather than the implementation details of MethodChannels) during the test. To do that, you need to be able to inject a mock that implements SharedPreferences in your Auth class. Right now, Auth is coupled not only to SharedPreferences, but to a particular way of getting a SharedPreferences instance. By removing the latter coupling, you can proceed with testing.

Ignoring async for a minute, here's a simple way of doing that using a global variable. (More structured approaches obviously exist using constructor injection or even a dependency injection framework. Often these lead you to move from static members to instances and instance members.)

// globals.dart
SharedPreferences sharedPreferences = SharedPreferences.getInstance();

// auth.dart
import 'globals.dart';

class Auth {
  static String sessionID() {
     SharedPreferences prefs = sharedPreferences;
     return prefs.getString(SID_KEY);
  }
}

// auth_test.dart
import 'package:mockito/mockito.dart';
import 'globals.dart';

class MockSharedPreferences extends Mock implements SharedPreferences {}

void main() {
  test('Auth knows session ID', () {
    var mock = new MockSharedPreferences();
    sharedPreferences = mock;
    when(mock.getString(SID_KEY)).thenReturn('some_sid');
    expect(Auth.sessionID(), 'some_id');
  });
}

Reintroducing async, you'll have to work with Future<SharedPreferences> instead of SharedPreferences in the above.

// globals.dart
Future<SharedPreferences> sharedPreferences = SharedPreferences.getInstance();

// auth.dart
import 'globals.dart';

class Auth {
  static Future<String> sessionID() async {
     SharedPreferences prefs = await sharedPreferences;
     return prefs.getString(SID_KEY);
  }
}

// auth_test.dart
import 'package:mockito/mockito.dart';
import 'globals.dart';

class MockSharedPreferences extends Mock implements SharedPreferences {}

void main() {
  test('Auth knows session ID', () async {
    var mock = new MockSharedPreferences();
    sharedPreferences = new Future.value(mock);
    when(mock.getString(SID_KEY)).thenReturn('some_sid');
    expect(await Auth.sessionID(), 'some_id');
  });
}

Nicholas Manning

unread,
Dec 17, 2017, 5:39:04 AM12/17/17
to Flutter Dev
Mikkel, 

This is quite helpful and thank you for this code. 

Right now, it seems like a lot of overhead for testing something simple. What about implementing an integration test?  I already plan on writing integration tests in order to test my UI so it may be much easier this way, no?

Mikkel Ravn

unread,
Dec 17, 2017, 6:13:51 AM12/17/17
to Nicholas Manning, Flutter Dev
It all depends on your testing objectives vs resources. If you want good unit test coverage at the class level, you need to design your production code for testability and work with mocks and injection. Same thing, if you want to do widget testing with flutter_test (component-level testing). Integration testing with flutter_driver or other UI robots allows you to avoid that, because now you are testing the full app on a device or simulator. But it obviously comes with its own non-trivial costs in terms of test development, test execution time, test robustness, and test maintenance. Plus, you are bound to find some aspects hard to test, because you have less control (e.g. error situations).

Nicholas Manning

unread,
Dec 18, 2017, 3:11:13 AM12/18/17
to Mikkel Ravn, Flutter Dev
Yep, makes sense. It'd be quite interesting to explore once an app is beyond the bootstrap stages for sure.  Thanks!
Reply all
Reply to author
Forward
0 new messages