Flutter의 단위 테스트: 서비스, 블록 및 Sqflite

21856 단어
"단위 테스트"를 Google에서 간단히 검색하면 다음과 같이 표시됩니다.

Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinised for proper operation



음, 위의 인용문은 매우 자명합니다. 추가하기 위해 테스트의 몇 가지 이점을 공유하겠습니다.
  • 실제 사용자가 버그를 찾기 전에 버그를 찾는 데 도움이 됩니다.
  • 확장 가능한 코드를 유지 관리하는 데 도움이 됩니다.
  • 업데이트된 코드가 기존 기능을 손상시키지 않도록 하는 데 도움이 됩니다.
  • 더 나은 개발자가 되는 데 도움이 됩니다👌

  • 개요



    우리는 테스트를 위해 간단한 메모 앱을 사용할 것입니다. 이 앱은 상태 관리를 위해 큐빗을 사용하고 스토리지를 위해 Sqflite를 사용합니다.

    sqlflite is a structured cache database that helps us perform SQL queries on the device cache.



    따라서 sqflite, CRUD 서비스 및 bloc 구현에 대한 단위 테스트를 작성하여 앱에서 예기치 않은 일이 발생하지 않도록 할 것입니다.

    종속성:



    다음 종속 항목을 설치합니다.

    dependencies:
      sqflite:
      bloc_test:
    
    dev_dependencies:
      mockito: ^5.2.0
      build_runner: ^2.2.0
      sqflite_common_ffi: ^2.1.1+1
    


    구현:



    우리는 앱에 두 개의 화면이 있고 모범 사례로 각 화면에는 자체 큐빗이 있어야 합니다. 그래서 우리는 모든 작업을 표시하는 홈 화면용으로 두 개의 큐빗을, 편집 화면용으로 다른 하나를 갖게 됩니다. 새 작업을 편집, 삭제 및 생성할 수 있습니다. 또한 전체 CRUD 작업을 관리하는 서비스 클래스도 있습니다.

    app_service.dart

    class TaskService {
      late Database db;
    
      TaskService._privateConstructor();
      static final TaskService instance = TaskService._privateConstructor();
    
      Future initialize(String path) async {
        db = await openDatabase(path, version: 1,
            onCreate: (Database db, int version) async {
          return await db.execute(databaseRules);
        });
      }
    
      Future<Task> insert(Task task) async {
        task.id = await db.insert(tableTodo, task.toMap());
        return task;
      }
    
      Future<List<Task>?> getAllTask() async {
        await Future.delayed(const Duration(milliseconds: 1000));
        var p = await db.rawQuery('SELECT * FROM $tableTodo');
        var list;
        list = List<Task>.from(p.map((e) => Task.fromMap(e)));
        return list;
      }
    
      Future<Task?> getTask(int id) async {
        List<Map> maps = await db.query(tableTodo,
            columns: [columnId, columnDone, columnTitle, columnDesc],
            where: '$columnId = ?',
            whereArgs: [id]);
        if (maps.isNotEmpty) {
          return Task.fromMap(maps.first as Map<String, dynamic>);
        }
        return null;
      }
    
      Future<int?> delete(int id) async {
        return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
      }
    
      Future<int?> update(Task task) async {
        return await db.update(tableTodo, task.toMap(),
            where: '$columnId = ?', whereArgs: [task.id]);
      }
    
      Future close() async => db.close();
    }
    
    


    home_cubit.dart

    class HomeScreenCubit extends Cubit<HomeScreenState> {
      late final TaskService taskService;
    
      HomeScreenCubit(TaskService service) : super(InitialState()) {
        taskService = service;
      }
    
      void fetchAllTask() async {
        emit(OnLoading());
        var p = await this.taskService.getAllTask();
        if (p != null) {
          if (p.isNotEmpty) {
            List<Task> _com = [];
            List<Task> _unCom = [];
            for (var element in p) {
              if (element.done == true) {
                _com.add(element);
              } else {
                _unCom.add(element);
              }
            }
            emit(OnSuccess(completedTasks: _com, unCompletedTasks: _unCom));
          } else {
            emit(OnEmpty());
          }
        } else {
          emit(OnFailure(error: ""));
        }
      }
    
      void updateTask(Task task) async {
        var p = await this.taskService.update(task);
        if (p != null) {
          var p = await this.taskService.getAllTask();
          if (p != null) {
            if (p.isNotEmpty) {
              List<Task> _com = [];
              List<Task> _unCom = [];
              for (var element in p) {
                if (element.done == true) {
                  _com.add(element);
                } else {
                  _unCom.add(element);
                }
              }
              emit(OnSuccess(completedTasks: _com, unCompletedTasks: _unCom));
            } else {
              emit(OnEmpty());
            }
          }
        } else {
          emit(OnUpdateFailure(error: ""));
        }
      }
    
      void updateList() async {
        var p = await this.taskService.getAllTask();
        if (p != null) {
          if (p.isNotEmpty) {
            List<Task> _com = [];
            List<Task> _unCom = [];
            for (var element in p) {
              if (element.done == true) {
                _com.add(element);
              } else {
                _unCom.add(element);
              }
            }
            emit(OnSuccess(completedTasks: _com, unCompletedTasks: _unCom));
          } else {
            emit(OnEmpty());
          }
        } else {
          emit(OnFailure(error: ""));
        }
      }
    }
    
    


    edit_screen_cubit.dart

    class EditScreenCubit extends Cubit<EditScreenState> {
      late final TaskService taskService;
    
      EditScreenCubit(TaskService service) : super(InitialEditState()) {
        taskService = service;
      }
    
      void createTask(
          {TaskService? taskService, required String title, String? desc}) async {
        emit(OnEditLoading());
        Task _t = Task(title: title, desc: desc, done: false);
        await this.taskService.insert(_t);
        emit(
          OnEditSuccess(),
        );
      }
    
      void updateTask(
          {TaskService? taskService,
          required int id,
          required String title,
          String? desc,
          required bool done}) async {
        emit(
          OnEditLoading(),
        );
        Task _t = Task(title: title, desc: desc, done: done, id: id);
        var p = await this.taskService.update(_t);
        if (p != null) {
          emit(
            OnEditUpdateSuccess(),
          );
        } else {
          emit(
            OnEditUpdateFailure(error: ""),
          );
        }
      }
    
      void deleteTask({TaskService? taskService, required int taskId}) async {
        emit(
          OnEditLoading(),
        );
        var p = await this.taskService.delete(taskId);
        if (p != null) {
          emit(
            OnEditDeleteSuccess(),
          );
        } else {
          emit(
            OnEditDeleteFailure(error: ""),
          );
        }
      }
    }
    
    


    따라서 먼저 테스트 폴더 내에 모의 서비스를 설정하고 모의 서비스로 큐빗을 테스트하기 전에 작동하는지 확인합니다. 먼저 다음과 같이 app_service_test.dart 기본 기능에 주석을 달아 모의 TaskService를 생성해 보겠습니다.

    @GenerateMocks([TaskService])
    void main() {}
    


    그런 다음 다음을 실행하십시오.

    flutter packages pub run build_runner build --delete-conflicting-outputs
    


    이는 app_service_test.dart와 동일한 디렉토리에 MockTaskService 클래스를 생성해야 합니다.

    테스트를 위해 종속성을 설정해 보겠습니다.

      late Database database;
      late MockTaskService taskService;
      Task testTask = Task(id: 1, title: "first ", done: false, desc: "yes");
      List<Task> taskList = List.generate(10, (index) => testTask);
      setUpAll(() async {
        sqfliteFfiInit();
        database = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
        await database.execute(databaseRules);
        taskService = MockTaskService();
        taskService.db = database;
        when(taskService.insert(any)).thenAnswer((_) async => testTask);
        when(taskService.update(any)).thenAnswer((_) async => 1);
        when(taskService.delete(any)).thenAnswer((_) async => 1);
        when(taskService.getTask(any)).thenAnswer((_) async => testTask);
        when(taskService.getAllTask()).thenAnswer((_) async => taskList);
      });
    
    


    우리가 수행한 작업은 SQL 데이터베이스 개체를 테스트용 데이터베이스 시뮬레이터인 databaseFactoryFfi.openDatabase(inMemoryDatabasePath)로 초기화한 다음 입력 및 예상 결과로 사용할 미리 정의된 작업 개체를 갖는 것입니다.when 함수는 해당 함수가 호출될 때마다 예상되는 것입니다.
    데이터베이스를 테스트해 보겠습니다.

    
    
      group('Database Test', () {
        test('sqflite version', () async {
          expect(await database.getVersion(), 0);
        });
        test('add Item to database', () async {
          var i = await database.insert(
              tableTodo, Task(title: "first ", done: false, desc: "yes").toMap());
          var p = await database.query(tableTodo);
          expect(p.length, i);
        });
        test('add three Items to database', () async {
          await database.insert(
              tableTodo, Task(title: "second", done: false, desc: "yes").toMap());
          await database.insert(
              tableTodo, Task(title: "third ", done: false, desc: "yes").toMap());
          await database.insert(
              tableTodo, Task(title: "fourth ", done: false, desc: "yes").toMap());
          var p = await database.query(tableTodo);
          expect(p.length, 4);
        });
        test('update first Item', () async {
          await database.update(tableTodo,
              Task(title: "Changed the first", done: false, desc: "yes").toMap(),
              where: '$columnId = ?', whereArgs: [1]);
          var p = await database.query(tableTodo);
          expect(p.first['title'], "Changed the first");
        });
        test('delete the first Item', () async {
          await database.delete(tableTodo, where: '$columnId = ?', whereArgs: [1]);
          var p = await database.query(tableTodo);
          expect(p.length, 3);
        });
        test('Close db', () async {
          await database.close();
          expect(database.isOpen, false);
        });
      });
    
    


    그래서 기본적으로 우리가 위에서 한 것은 db에 저장하고 성공적인 실행을 기반으로 응답을 받는 시뮬레이션이었습니다.

    위의 db 인스턴스로 서비스를 테스트해 보겠습니다.

     group("Service test", () {
        test("create task", () async {
          verifyNever(taskService.insert(testTask));
          expect(await taskService.insert(testTask), testTask);
          verify(taskService.insert(testTask)).called(1);
        });
        test("update task", () async {
          verifyNever(taskService.update(testTask));
          expect(await taskService.update(testTask), 1);
          verify(taskService.update(testTask)).called(1);
        });
        test("delete task", () async {
          verifyNever(taskService.delete(1));
          expect(await taskService.delete(1), 1);
          verify(taskService.delete(1)).called(1);
        });
        test("get task", () async {
          verifyNever(taskService.getTask(1));
          expect(await taskService.getTask(1), testTask);
          verify(taskService.getTask(1)).called(1);
        });
        test("get all task", () async {
          verifyNever(taskService.getAllTask());
          expect(await taskService.getAllTask(), taskList);
          verify(taskService.getAllTask()).called(1);
        });
      });
    


    그래서 우리는 기본적으로 테스트 가능한 db를 사용하여 서비스를 테스트했으며 아무 것도 중단되지 않고 모든 것이 예상대로 작동한다고 확신합니다. 여기에 완전한 app_service_test.dart 파일이 있습니다.

    @GenerateMocks([TaskService])
    void main() {
      late Database database;
      late MockTaskService taskService;
      Task testTask = Task(id: 1, title: "first ", done: false, desc: "yes");
      List<Task> taskList = List.generate(10, (index) => testTask);
      setUpAll(() async {
        sqfliteFfiInit();
        database = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
        await database.execute(databaseRules);
        taskService = MockTaskService();
        taskService.db = database;
        when(taskService.insert(any)).thenAnswer((_) async => testTask);
        when(taskService.update(any)).thenAnswer((_) async => 1);
        when(taskService.delete(any)).thenAnswer((_) async => 1);
        when(taskService.getTask(any)).thenAnswer((_) async => testTask);
        when(taskService.getAllTask()).thenAnswer((_) async => taskList);
      });
    
      group('Database Test', () {
        test('sqflite version', () async {
          expect(await database.getVersion(), 0);
        });
        test('add Item to database', () async {
          var i = await database.insert(
              tableTodo, Task(title: "first ", done: false, desc: "yes").toMap());
          var p = await database.query(tableTodo);
          expect(p.length, i);
        });
        test('add three Items to database', () async {
          await database.insert(
              tableTodo, Task(title: "second", done: false, desc: "yes").toMap());
          await database.insert(
              tableTodo, Task(title: "third ", done: false, desc: "yes").toMap());
          await database.insert(
              tableTodo, Task(title: "fourth ", done: false, desc: "yes").toMap());
          var p = await database.query(tableTodo);
          expect(p.length, 4);
        });
        test('update first Item', () async {
          await database.update(tableTodo,
              Task(title: "Changed the first", done: false, desc: "yes").toMap(),
              where: '$columnId = ?', whereArgs: [1]);
          var p = await database.query(tableTodo);
          expect(p.first['title'], "Changed the first");
        });
        test('delete the first Item', () async {
          await database.delete(tableTodo, where: '$columnId = ?', whereArgs: [1]);
          var p = await database.query(tableTodo);
          expect(p.length, 3);
        });
        test('Close db', () async {
          await database.close();
          expect(database.isOpen, false);
        });
      });
    
      group("Service test", () {
        test("create task", () async {
          verifyNever(taskService.insert(testTask));
          expect(await taskService.insert(testTask), testTask);
          verify(taskService.insert(testTask)).called(1);
        });
        test("update task", () async {
          verifyNever(taskService.update(testTask));
          expect(await taskService.update(testTask), 1);
          verify(taskService.update(testTask)).called(1);
        });
        test("delete task", () async {
          verifyNever(taskService.delete(1));
          expect(await taskService.delete(1), 1);
          verify(taskService.delete(1)).called(1);
        });
        test("get task", () async {
          verifyNever(taskService.getTask(1));
          expect(await taskService.getTask(1), testTask);
          verify(taskService.getTask(1)).called(1);
        });
        test("get all task", () async {
          verifyNever(taskService.getAllTask());
          expect(await taskService.getAllTask(), taskList);
          verify(taskService.getAllTask()).called(1);
        });
      });
    }
    


    완척:



    Cubit은 상태 관리이며 단계를 건너뛰지 않고 적절한 시간에 올바른 상태를 방출하는지 확인하기 위해 테스트할 것입니다. 따라서 두 개의 테스트 파일이 있을 것입니다.

    home_cubit_test.dart

    void main() {
      late HomeScreenCubit mockCubit;
      late MockTaskService mockService;
      late Task testTask = Task(id: 1, title: "first ", done: false, desc: "yes");
      List<Task> taskList = List.generate(10, (index) => testTask);
    
      setUp(() async {
        mockService = MockTaskService();
        mockCubit = HomeScreenCubit(mockService);
      });
    


    그래서 우리가 위에서 한 것은 MockTaskService로 큐빗을 초기화하고 테스트할 미리 정의된 데이터를 사용하는 것입니다.
    전체 테스트는 다음과 같습니다.

    void main() {
      late HomeScreenCubit mockCubit;
      late MockTaskService mockService;
      late Task testTask = Task(id: 1, title: "first ", done: false, desc: "yes");
      List<Task> taskList = List.generate(10, (index) => testTask);
    
      setUp(() async {
        mockService = MockTaskService();
        mockCubit = HomeScreenCubit(mockService);
      });
      group("Home Screen bloc test", () {
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if fetch all works',
          build: () => mockCubit,
          setUp: () =>
              when(mockService.getAllTask()).thenAnswer((_) async => taskList),
          act: (b) => b.fetchAllTask(),
          expect: () => [isA<OnLoading>(), isA<OnSuccess>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if fetch all is empty',
          build: () => mockCubit,
          setUp: () => when(mockService.getAllTask()).thenAnswer((_) async => []),
          act: (b) => b.fetchAllTask(),
          expect: () => [isA<OnLoading>(), isA<OnEmpty>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if fetch all fails',
          build: () => mockCubit,
          setUp: () => when(mockService.getAllTask()).thenAnswer((_) async => null),
          act: (b) => b.fetchAllTask(),
          expect: () => [isA<OnLoading>(), isA<OnFailure>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if update task works',
          build: () => mockCubit,
          setUp: () {
            when(mockService.update(any)).thenAnswer((_) async => testTask.id!);
            when(mockService.getAllTask()).thenAnswer((_) async => taskList);
          },
          act: (b) => b.updateTask(testTask),
          expect: () => [isA<OnSuccess>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if update task is empty',
          build: () => mockCubit,
          setUp: () {
            when(mockService.update(any)).thenAnswer((_) async => testTask.id!);
            when(mockService.getAllTask()).thenAnswer((_) async => []);
          },
          act: (b) => b.updateTask(testTask),
          expect: () => [isA<OnEmpty>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if update task fails',
          build: () => mockCubit,
          setUp: () {
            when(mockService.update(any)).thenAnswer((_) async => null);
            when(mockService.getAllTask()).thenAnswer((_) async => taskList);
          },
          act: (b) => b.updateTask(testTask),
          expect: () => [isA<OnUpdateFailure>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if update all works',
          build: () => mockCubit,
          setUp: () =>
              when(mockService.getAllTask()).thenAnswer((_) async => taskList),
          act: (b) => b.updateList(),
          expect: () => [isA<OnSuccess>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if update all fails',
          build: () => mockCubit,
          setUp: () => when(mockService.getAllTask()).thenAnswer((_) async => null),
          act: (b) => b.updateList(),
          expect: () => [isA<OnFailure>()],
        );
        blocTest<HomeScreenCubit, HomeScreenState>(
          'check if update all is empty',
          build: () => mockCubit,
          setUp: () => when(mockService.getAllTask()).thenAnswer((_) async => []),
          act: (b) => b.updateList(),
          expect: () => [isA<OnEmpty>()],
        );
      });
      tearDown(() => mockCubit.close());
    }
    
    


    bloc_test: 블록을 빌드합니다.
    빌드: 블록을 초기화하고 테스트 준비를 합니다.
    설정: 분석법에서 원하는 사전 정의된 모든 조건입니다.
    act: 실행하려는 bloc 메서드를 호출하는 곳입니다.
    expect: 기대하는 순서대로 예상되는 상태의 배열입니다.
    tearDown: 이 함수는 모든 테스트 사례를 실행한 후 호출되며 블록, 스트림 등을 닫는 일반적인 위치입니다.

    edit_cubit_test.dart

    void main() {
      late EditScreenCubit mockCubit;
      late MockTaskService mockService;
      late Task testTask = Task(id: 1, title: "first ", done: false, desc: "yes");
    
      setUp(() async {
        mockService = MockTaskService();
        mockCubit = EditScreenCubit(mockService);
      });
      group('EditScreenBloc', () {
        blocTest<EditScreenCubit, EditScreenState>(
          'check if create was successful',
          build: () => mockCubit,
          setUp: () =>
              when(mockService.insert(any)).thenAnswer((_) async => testTask),
          act: (b) => b.createTask(title: testTask.title!),
          expect: () => [isA<OnEditLoading>(), isA<OnEditSuccess>()],
        );
        blocTest<EditScreenCubit, EditScreenState>(
          'check if update was successful',
          build: () => mockCubit,
          setUp: () => when(mockService.update(any)).thenAnswer((_) async => 1),
          act: (b) => b.updateTask(
              title: testTask.title!, id: testTask.id!, done: testTask.done!),
          expect: () => [isA<OnEditLoading>(), isA<OnEditUpdateSuccess>()],
        );
        blocTest<EditScreenCubit, EditScreenState>(
          'check if update failed',
          build: () => mockCubit,
          setUp: () => when(mockService.update(any)).thenAnswer((_) async => null),
          act: (b) => b.updateTask(
              title: testTask.title!, id: testTask.id!, done: testTask.done!),
          expect: () => [isA<OnEditLoading>(), isA<OnEditUpdateFailure>()],
        );
        blocTest<EditScreenCubit, EditScreenState>(
          'check if delete was successful',
          build: () => mockCubit,
          setUp: () => when(mockService.delete(any)).thenAnswer((_) async => 1),
          act: (b) => b.deleteTask(taskId: testTask.id!),
          expect: () => [isA<OnEditLoading>(), isA<OnEditDeleteSuccess>()],
        );
        blocTest<EditScreenCubit, EditScreenState>(
          'check if delete failed',
          build: () => mockCubit,
          setUp: () => when(mockService.delete(any)).thenAnswer((_) async => null),
          act: (b) => b.deleteTask(taskId: testTask.id!),
          expect: () => [isA<OnEditLoading>(), isA<OnEditDeleteFailure>()],
        );
      });
      tearDown(() => mockCubit.close());
    }
    


    레포: Odinote github repo

    즐겁게 읽으셨기를 바랍니다.

    좋은 웹페이지 즐겨찾기