対象者
SharedPreferences
を使ったユニットテストをしたいFlutter開発者- テストの安定性を高めたいCI/QAエンジニア
はじめに
Flutterにおいて、簡単なデータ保存を行う場合、まず検討されるのがSharedPreferences
です。しかし実機やエミュレータ上でテストを行う際、初期データが空の状態やあらかじめデータが入っている状態を再現するのは難しく、テストの作成や実行が困難になります。
そこで役立つのが、SharedPreferences.setMockInitialValues
を使ったモックテストです。このメソッドを利用すれば、ユニットテスト内で任意の初期状態を再現し、SharedPreferences
の動作を検証できます。これにより、データ保存や取得の正しさを安全に確認することが可能になります。ただし、setMockInitialValues
は静的メソッドであるため、モックデータがテスト間で引き継がれ、意図せずほかのテストに干渉してしまう問題があります。本記事では、その注意点と対策について詳しく解説していきます。
SharedPreferencesのテスト方法
SharedPreferences
の基本的なテスト方法は、他のユニットテストと同様に test
/expect
を使って実施します。ただし、実際のアプリでは起動時点で既にデータが保存されている可能性があるため、テスト実行前に任意の初期状態を再現する必要があります。
モック初期化
SharedPreferences.setMockInitialValues
を使うことで、アプリ起動時にあたかも既存データが入っているかのような状態を再現できます。
// 起動時に 'foo':'bar' が入っている状態をモック
SharedPreferences.setMockInitialValues({'foo': 'bar'});
テスト
// モックを初期化したら、SharedPreferences.getInstance() でインスタンスを取得します。
final prefs = await SharedPreferences.getInstance();
// 初期化されたモック上で、getString や setString、remove など通常通りのメソッドを呼び出して
// 動作を検証します。
// 初期化した値の検証
expect(prefs.getString('foo'), 'bar');
// 値の書き換えと再取得
prefs.setString('baz', 'qux');
expect(prefs.getString('baz'), 'qux');
おすすめのテスト方法
以下のように、各テスト実行前に必ず初期化を行う setUp
を定義すると、テストのたびにクリーンな状態で SharedPreferences
を使うことができます。
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
group('SharedPreferences テスト', () {
setUp(() {
// 毎回モックを空の状態で初期化
SharedPreferences.setMockInitialValues({});
});
test('初期状態が空か確認', () async {
final pref = await SharedPreferences.getInstance();
expect(pref.getString('key'), isNull);
});
test('特定の値で初期化して動作確認', () async {
SharedPreferences.setMockInitialValues({'test': 'value'});
final pref = await SharedPreferences.getInstance();
expect(pref.getString('test'), 'value');
pref.setString('foo', 'bar');
expect(pref.getString('foo'), 'bar');
});
test('前のテストの影響を受けない', () async {
final pref = await SharedPreferences.getInstance();
expect(pref.getString('test'), isNull);
expect(pref.getString('foo'), isNull);
});
});
}
なぜ毎回初期化した方が良いか
-
静的メソッドの副作用
SharedPreferences.setMockInitialValues
は静的に保持されるため、一度設定した値が次のテストでも残ってしまう。 -
テスト間の干渉
モックの状態が保持されることで、あるテストケースの前提が壊れ、不安定なテスト結果になる。 -
setUp
で毎回初期化のメリット- テスト実行前に必ず状態をリセット
- 各テストが独立して動作
- テストの再現性と信頼性が向上
運用のヒント
-
バージョンアップ時の項目追加に注意
- リリース後に「バージョン1では存在しなかったキー」をバージョン2で追加するケースがよく発生します。
- 既存ユーザーの端末には前バージョンのデータフォーマットが残っているため、新バージョンで新しいキーを必須扱いにすると問題が起こりやすいです。
-
新しい項目の扱い方
- Nullable にする
- 追加したキーは戻り値をnullable(
String?
やint?
など)にして、旧バージョンのデータでもエラーが出ないようにします。
- 追加したキーは戻り値をnullable(
- デフォルト値を設定する
getString('newKey') ?? 'デフォルト値'
のように、キーが存在しない場合でも安全に動作するコードを用意します。
- Nullable にする
また、SharedPreferenceのバージョン自体を保存することで、動作を変更するのも良いかもしれません。
実例
以下の想定でテストを作成ます。
-
バージョン1
- 保存されているキーは
key1
のみ
- 保存されているキーは
-
バージョン2
- 新たに
key2
を追加 - 既存ユーザー(バージョン1 からアップデートした場合)でもクラッシュしないよう、起動時に
key2
が存在しない場合はデフォルト値を返す - 保存されるデータは
key1
とkey2
の両方を保持可能
- 新たに
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
group('Version2 アプリ起動時の SharedPreferences テスト', () {
/// Version2 で追加した key2 を取得する際のデフォルト値を定義
const defaultKey2Value = 'default_value';
/// Version2 アプリの起動時ロジック例
Future<Map<String, String?>> loadVersion2Prefs() async {
final prefs = await SharedPreferences.getInstance();
// Version1 では key1 しか存在しないため key1 は null または当該値
final key1 = prefs.getString('key1');
// Version2 で追加した key2 がない場合、デフォルト値を返す
final key2 = prefs.getString('key2') ?? defaultKey2Value;
return {'key1': key1, 'key2': key2};
}
test('データがない状態(初回インストール/空の SharedPreferences)', () async {
// ■ モック初期化:何も保存されていない状態
SharedPreferences.setMockInitialValues({});
final result = await loadVersion2Prefs();
// key1 は存在しないので null
expect(result['key1'], isNull);
// key2 は存在しないのでデフォルト値が返る
expect(result['key2'], defaultKey2Value);
});
test('バージョン1 のデータがある状態(key1 のみ保存済み)', () async {
// ■ モック初期化:Version1 のみのデータを再現
SharedPreferences.setMockInitialValues({'key1': 'value1_v1'});
final result = await loadVersion2Prefs();
// key1 は Version1 の値を取得
expect(result['key1'], 'value1_v1');
// key2 はまだ存在しないのでデフォルト値
expect(result['key2'], defaultKey2Value);
});
test('バージョン2 のデータがある状態(key1 と key2 の両方が保存済み)', () async {
// ■ モック初期化:Version2 のデータを再現
SharedPreferences.setMockInitialValues({
'key1': 'value1_v2',
'key2': 'value2_v2',
});
final result = await loadVersion2Prefs();
// key1 は Version2 の値を取得
expect(result['key1'], 'value1_v2');
// key2 は Version2 の値を取得
expect(result['key2'], 'value2_v2');
});
});
}
説明:
-
loadVersion2Prefs()
- Version2 アプリ起動時に呼び出す想定のメソッドです。
key1
はそのまま取得し、存在しない場合はnull
。key2
は Version2 で追加された項目なので、存在しない場合はdefaultKey2Value
を返すようにしています。
-
テストケース
-
データがない状態
setMockInitialValues({})
で空の SharedPreferences を再現。key1
はnull
、key2
にはデフォルト値が返ることを確認。
-
バージョン1 のデータだけある状態
{'key1': 'value1_v1'}
をセットして、Version1 ユーザーデータを再現。key1
は'value1_v1'
、key2
はデフォルト値になることを検証。
-
バージョン2 のデータがある状態
{'key1': 'value1_v2', 'key2': 'value2_v2'}
をセットして、Version2 ユーザーデータを再現。key1
とkey2
の両方が正しく取得できることを確認。
-
このように、SharedPreferences.setMockInitialValues
のモック初期化を使うことで、実機を用いずに「データがない状態」「Version1 のみの状態」「Version2 の状態」をテストし、Version2 アプリの挙動を自動で検証できます。
まとめ
SharedPreferences.setMockInitialValues
は便利だが静的なためテスト間で値が引き継がれる問題がある。- その問題を防ぐには、
setUp
の中で毎回モックを初期化することがベストプラクティス。 - これにより、どのテストも常にクリーンな状態で実行でき、安定した結果が得られる。
ソース(main.dartにコピペして動作確認用)
Flutter: 3.29.2
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
group('SharedPreferences テスト', () {
setUp(() {
// 毎回モックを空の状態で初期化
SharedPreferences.setMockInitialValues({});
});
test('初期状態が空か確認', () async {
final pref = await SharedPreferences.getInstance();
expect(pref.getString('key'), isNull);
});
test('特定の値で初期化して動作確認', () async {
SharedPreferences.setMockInitialValues({'test': 'value'});
final pref = await SharedPreferences.getInstance();
expect(pref.getString('test'), 'value');
pref.setString('foo', 'bar');
expect(pref.getString('foo'), 'bar');
});
test('前のテストの影響を受けない', () async {
final pref = await SharedPreferences.getInstance();
expect(pref.getString('test'), isNull);
expect(pref.getString('foo'), isNull);
});
});
group('SharedPreferencesの初期化のテスト', () {
test('初期化', () async {
SharedPreferences.setMockInitialValues({'test': 'test'});
final pref = await SharedPreferences.getInstance();
expect(pref.getString('test'), isNotNull);
expect(pref.getString('test'), 'test');
expect(pref.getString('test2'), isNull);
pref.setString('test2', 'test2');
expect(pref.getString('test2'), 'test2');
});
test('干渉するか→する', () async {
final pref = await SharedPreferences.getInstance();
expect(pref.getString('test'), 'test');
});
test('毎回初期化する', () async {
SharedPreferences.setMockInitialValues({});
final pref = await SharedPreferences.getInstance();
expect(pref.getString('test'), isNull);
});
});
group('Version2 アプリ起動時の SharedPreferences テスト', () {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
/// Version2 で追加した key2 を取得する際のデフォルト値を定義
const defaultKey2Value = 'default_value';
/// Version2 アプリの起動時ロジック例
Future<Map<String, String?>> loadVersion2Prefs() async {
final prefs = await SharedPreferences.getInstance();
// Version1 では key1 しか存在しないため key1 は null または当該値
final key1 = prefs.getString('key1');
// Version2 で追加した key2 がない場合、デフォルト値を返す
final key2 = prefs.getString('key2') ?? defaultKey2Value;
return {'key1': key1, 'key2': key2};
}
test('データがない状態(初回インストール/空の SharedPreferences)', () async {
final result = await loadVersion2Prefs();
// key1 は存在しないので null
expect(result['key1'], isNull);
// key2 は存在しないのでデフォルト値が返る
expect(result['key2'], defaultKey2Value);
});
test('バージョン1 のデータがある状態(key1 のみ保存済み)', () async {
// ■ モック初期化:Version1 のみのデータを再現
SharedPreferences.setMockInitialValues({'key1': 'value1_v1'});
final result = await loadVersion2Prefs();
expect(result['key1'], 'value1_v1');
expect(result['key2'], defaultKey2Value);
});
test('バージョン2 のデータがある状態(key1 と key2 の両方が保存済み)', () async {
// ■ モック初期化:Version2 のデータを再現
SharedPreferences.setMockInitialValues({
'key1': 'value1_v2',
'key2': 'value2_v2',
});
final result = await loadVersion2Prefs();
expect(result['key1'], 'value1_v2');
expect(result['key2'], 'value2_v2');
});
});
}