Flutter의 단위 테스트: 서비스, 블록 및 Sqflite
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
즐겁게 읽으셨기를 바랍니다.
Reference
이 문제에 관하여(Flutter의 단위 테스트: 서비스, 블록 및 Sqflite), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/odinachi/unit-testing-in-flutter-services-blocs-and-sqflite-ga5텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)