😇

Expected: [Instance of 'Note'] Actual: [Instance of 'Note'] Which: at

2024/06/27に公開

sqfliteでモックのテストをしたいけどできない???

sqfliteでテストコードを書いていたが、専用のパッケージが必要らしい😅
mocktailだけでは足りないようだ?
sqflite_common_ffiを追加するのか。

https://pub.dev/packages/sqflite_common_ffi
https://pub.dev/packages/mocktail

テストに使ったプロジェクト

📝テストコードを書く

sqfliteを使うには、面倒くさい設定を書く😅
NoSQLでいいどだろう!
ObjectBoxってのがあるのに笑
Realmってのもあるけどね。Isarもあるか。

しかしメンテナンスが継続されているパッケージを好むのが仕事してる人なのですよ。

[これを書く]

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_app/domain/entity/note_model.dart';

// データべースの操作を行うクラス
class DatabaseHelper {
  // データベースのバージョン
  static const int _version = 1;
  // データベースの名前
  static const String _dbName = 'Notes.db';
  // データベースのインスタンス
  Future<Database> _getDB() async {
    // データベースのパスを取得
    return openDatabase(
      // データベースのパスを指定
      join(await getDatabasesPath(), _dbName),
      // データベースのバージョンを指定
      version: _version,
      // データベースを作成する関数を指定
      onCreate: (db, version) async =>
      // データベースにテーブルを作成
      await db.execute(
        "CREATE TABLE Note(id INTEGER PRIMARY KEY, title TEXT NOT NULL, description TEXT NOT NULL)"
      ),
    );
  }
  // データベースにデータを追加, int型なのは追加したデータのidを返すため
  Future<int> addNote(Note note) async {
    // データベースのインスタンスを取得
    final db = await _getDB();
    // データベースにデータを追加
    return await db.insert(
      'Note',
      note.toJson(),// モデルクラスをMap型に変換
      conflictAlgorithm: ConflictAlgorithm.replace,// データが重複した場合は置き換える
    );
  }
  // データベースのデータを更新, int型なのは更新したデータのidを返すため
  Future<int> updateNote(Note note) async {
    final db = await _getDB();
    return await db.update(
      'Note',
      note.toJson(),
      where: 'id = ?',
      whereArgs: [note.id],
      conflictAlgorithm: ConflictAlgorithm.replace
    );
  }
  // データベースのデータを削除, int型なのは削除したデータのidを返すため
  Future<int> deleteNote(Note note) async {
    final db = await _getDB();
    return await db.delete(
      'Note',
      where: 'id = ?',
      whereArgs: [note.id],
    );
  }
  // データベースのデータを全て取得
  Future<List<Note>?> getAllNote() async {
    final db = await _getDB();

    final List<Map<String, dynamic>> maps = await db.query('Note');

    if(maps.isEmpty) {
      return null;
    } else {
      return List.generate(maps.length, (index) => Note.fromJson(maps[index]));
    }
  }
}

モックのパッケージを追加したら、テストコードを書いてみる。

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:sqflite_app/application/mock_service.dart/database_helper.dart';
import 'package:sqflite_app/domain/entity/note_model.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';

// データベースの初期化
class MockDatabase extends Mock implements Database {}

void main() {
  // テストの実行前に一度だけ実行されるセットアップ関数
  setUpAll(() {
    // sqflite_common_ffiを初期化
    sqfliteFfiInit();
    // データベースファクトリをFFIバージョンに設定
    databaseFactory = databaseFactoryFfi;
  });

  // mocktailを使用したテスト
  test('addNote', () async {
    final db = MockDatabase();
    final note = Note(
      id: 1,
      title: 'title',
      description: 'description',
    );
    // DatabaseHelperの_getDBメソッドをモック化するためにDatabaseHelperを拡張
    final databaseHelper = DatabaseHelperMock(db);
    when(() => db.insert(
      any(),
      any(),
      conflictAlgorithm: any(named: 'conflictAlgorithm'),
    )).thenAnswer((_) async => 1);

    final result = await databaseHelper.addNote(
      note,
    );
    expect(result, 1);
  });
  // mockのデータがselectで取得できるかテスト
  test('getNotes', () async {
    final db = MockDatabase();
    final note = Note(
      id: 1,
      title: 'title',
      description: 'description',
    );
    final databaseHelper = DatabaseHelperMock(db);
    when(() => db.query(
      any(),
      columns: any(named: 'columns'),
      where: any(named: 'where'),
      whereArgs: any(named: 'whereArgs'),
    )).thenAnswer((_) async => [
      {
        'id': note.id,
        'title': note.title,
        'description': note.description,
      }
    ]);

    final result = await databaseHelper.getAllNote();
    expect(result, [note]);
  });
}

// DatabaseHelperを拡張して_getDBメソッドをオーバーライド
class DatabaseHelperMock extends DatabaseHelper {
  final Database mockDb;

  DatabaseHelperMock(this.mockDb);

}

このまま実行するとエラーが出る😅

Expected: [Instance of 'Note'] Actual: [Instance of 'Note'] Which: at location [0] is <Instance of 'Note'> instead of <Instance of 'Note'>

package:matcher expect package:flutter_test/src/widget_tester.dart 474:18 expect test/unit_test/mock_controller_test.dart 63:5 main.<fn>

Github Copilotよ、俺ちゃんに力を・:*+.(( °ω° ))/.:+

このエラーメッセージは、expect関数が期待するNoteインスタンスと実際に返されたNoteインスタンスがメモリ上で異なるオブジェクトであることを示しています。expect関数はデフォルトでオブジェクトの同一性(同じインスタンスであるか)をチェックしますが、このケースではオブジェクトの内容の等価性(同じフィールド値を持つか)をチェックする必要があります。

この問題を解決するには、Noteクラスに==演算子とhashCodeメソッドをオーバーライドして、オブジェクトの内容に基づいた等価性を定義する必要があります。また、テストのアサーションを修正して、リスト内の各オブジェクトが期待する値と等しいかをチェックするようにします。

以下は、Noteクラスに==演算子とhashCodeメソッドをオーバーライドする方法の例です:

class Note {
  final int id;
  final String title;
  final String description;

  Note({required this.id, required this.title, required this.description});

  
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Note &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          title == other.title &&
          description == other.description;

  
  int get hashCode => id.hashCode ^ title.hashCode ^ description.hashCode;
}

モデルクラスを修正する

// メモ帳のモデルクラス
class Note {
  final int? id;// null許容型
  final String title;
  final String description;

  Note({
    this.id,
    required this.title,
    required this.description,
  });

  
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Note &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          title == other.title &&
          description == other.description;

  
  int get hashCode => id.hashCode ^ title.hashCode ^ description.hashCode;
  
  // データベースから取得したデータをモデルクラスに変換する
  factory Note.fromJson(Map<String, dynamic> json) => Note(
        id: json['id'],
        title: json['title'],
        description: json['description'],
      );
  // モデルクラスをデータベースに保存するためにMap型に変換する
  Map<String, dynamic> toJson() => {
        'id': id,
        'title': title,
        'description': description,
      };
}

おおおテストコードが通ったぞ笑

最後に

テストコードを書くときは、ローカルDBを使うなら、専用のパッケージも必要だったようです。気づかなくてハマりました💦
ご興味ある方は試してみてください。どうやらテスト書かない人はダメな人のようです😇
品質大事

Discussion