【Flutter】SharedPreferencesをモックでテスト

  • 2025年6月6日
  • 2025年6月6日
  • test

対象者

  • SharedPreferences を使ったユニットテストをしたいFlutter開発者
  • テストの安定性を高めたいCI/QAエンジニア

はじめに

Flutterにおいて、簡単なデータ保存を行う場合、まず検討されるのがSharedPreferencesです。しかし実機やエミュレータ上でテストを行う際、初期データが空の状態やあらかじめデータが入っている状態を再現するのは難しく、テストの作成や実行が困難になります。
そこで役立つのが、SharedPreferences.setMockInitialValuesを使ったモックテストです。このメソッドを利用すれば、ユニットテスト内で任意の初期状態を再現し、SharedPreferencesの動作を検証できます。これにより、データ保存や取得の正しさを安全に確認することが可能になります。ただし、setMockInitialValuesは静的メソッドであるため、モックデータがテスト間で引き継がれ、意図せずほかのテストに干渉してしまう問題があります。本記事では、その注意点と対策について詳しく解説していきます。

SharedPreferencesのテスト方法

SharedPreferences の基本的なテスト方法は、他のユニットテストと同様に testexpect を使って実施します。ただし、実際のアプリでは起動時点で既にデータが保存されている可能性があるため、テスト実行前に任意の初期状態を再現する必要があります。

モック初期化

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);
    });
  });
}

なぜ毎回初期化した方が良いか

  1. 静的メソッドの副作用
    SharedPreferences.setMockInitialValues は静的に保持されるため、一度設定した値が次のテストでも残ってしまう。

  2. テスト間の干渉
    モックの状態が保持されることで、あるテストケースの前提が壊れ、不安定なテスト結果になる。

  3. setUp で毎回初期化のメリット

    • テスト実行前に必ず状態をリセット
    • 各テストが独立して動作
    • テストの再現性と信頼性が向上

運用のヒント

  • バージョンアップ時の項目追加に注意

    • リリース後に「バージョン1では存在しなかったキー」をバージョン2で追加するケースがよく発生します。
    • 既存ユーザーの端末には前バージョンのデータフォーマットが残っているため、新バージョンで新しいキーを必須扱いにすると問題が起こりやすいです。
  • 新しい項目の扱い方

    1. Nullable にする
      • 追加したキーは戻り値をnullable(String?int? など)にして、旧バージョンのデータでもエラーが出ないようにします。
    2. デフォルト値を設定する
      • getString('newKey') ?? 'デフォルト値' のように、キーが存在しない場合でも安全に動作するコードを用意します。

また、SharedPreferenceのバージョン自体を保存することで、動作を変更するのも良いかもしれません。

実例

以下の想定でテストを作成ます。

  • バージョン1

    • 保存されているキーは key1 のみ
  • バージョン2

    • 新たに key2 を追加
    • 既存ユーザー(バージョン1 からアップデートした場合)でもクラッシュしないよう、起動時に key2 が存在しない場合はデフォルト値を返す
    • 保存されるデータは key1key2 の両方を保持可能
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');
    });
  });

}

説明:

  1. loadVersion2Prefs()

    • Version2 アプリ起動時に呼び出す想定のメソッドです。
    • key1 はそのまま取得し、存在しない場合は null
    • key2 は Version2 で追加された項目なので、存在しない場合は defaultKey2Value を返すようにしています。
  2. テストケース

    • データがない状態

      • setMockInitialValues({}) で空の SharedPreferences を再現。
      • key1nullkey2 にはデフォルト値が返ることを確認。
    • バージョン1 のデータだけある状態

      • {'key1': 'value1_v1'} をセットして、Version1 ユーザーデータを再現。
      • key1'value1_v1'key2 はデフォルト値になることを検証。
    • バージョン2 のデータがある状態

      • {'key1': 'value1_v2', 'key2': 'value2_v2'} をセットして、Version2 ユーザーデータを再現。
      • key1key2 の両方が正しく取得できることを確認。

このように、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');
    });
  });
}