사용자 마스터 UI 및 테스트 구현
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
||||
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
|
||||
import 'package:superport_v2/features/masters/user/presentation/controllers/user_controller.dart';
|
||||
|
||||
class _MockUserRepository extends Mock implements UserRepository {}
|
||||
|
||||
class _MockGroupRepository extends Mock implements GroupRepository {}
|
||||
|
||||
class _FakeUserInput extends Fake implements UserInput {}
|
||||
|
||||
void main() {
|
||||
late UserController controller;
|
||||
late _MockUserRepository userRepository;
|
||||
late _MockGroupRepository groupRepository;
|
||||
|
||||
final sampleUser = UserAccount(
|
||||
id: 1,
|
||||
employeeNo: 'A001',
|
||||
employeeName: '홍길동',
|
||||
email: 'hong@superport.com',
|
||||
group: UserGroup(id: 2, groupName: '관리자'),
|
||||
);
|
||||
|
||||
PaginatedResult<UserAccount> createResult({List<UserAccount>? items}) {
|
||||
final list = items ?? [sampleUser];
|
||||
return PaginatedResult<UserAccount>(
|
||||
items: list,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: list.length,
|
||||
);
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeUserInput());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
userRepository = _MockUserRepository();
|
||||
groupRepository = _MockGroupRepository();
|
||||
controller = UserController(
|
||||
userRepository: userRepository,
|
||||
groupRepository: groupRepository,
|
||||
);
|
||||
});
|
||||
|
||||
test('loadGroups 호출 시 그룹 목록 저장', () async {
|
||||
when(
|
||||
() => groupRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Group>(
|
||||
items: [Group(id: 1, groupName: '관리자')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await controller.loadGroups();
|
||||
|
||||
expect(controller.groups, isNotEmpty);
|
||||
});
|
||||
|
||||
group('fetch', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => groupRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Group>(
|
||||
items: [Group(id: 2, groupName: '관리자')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('정상 조회', () async {
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.result?.items, isNotEmpty);
|
||||
verify(
|
||||
() => userRepository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: null,
|
||||
groupId: null,
|
||||
isActive: null,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('에러 발생 시 errorMessage 설정', () async {
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenThrow(Exception('fail'));
|
||||
|
||||
await controller.fetch();
|
||||
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('필터 업데이트', () {
|
||||
controller.updateQuery('hong');
|
||||
controller.updateGroupFilter(2);
|
||||
controller.updateStatusFilter(UserStatusFilter.inactiveOnly);
|
||||
|
||||
expect(controller.query, 'hong');
|
||||
expect(controller.groupFilter, 2);
|
||||
expect(controller.statusFilter, UserStatusFilter.inactiveOnly);
|
||||
});
|
||||
|
||||
group('mutations', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async => createResult());
|
||||
});
|
||||
|
||||
final input = UserInput(
|
||||
employeeNo: 'A001',
|
||||
employeeName: '홍길동',
|
||||
groupId: 2,
|
||||
);
|
||||
|
||||
test('create 성공', () async {
|
||||
when(
|
||||
() => userRepository.create(any()),
|
||||
).thenAnswer((_) async => sampleUser);
|
||||
|
||||
final created = await controller.create(input);
|
||||
|
||||
expect(created, isNotNull);
|
||||
verify(() => userRepository.create(any())).called(1);
|
||||
});
|
||||
|
||||
test('update 성공', () async {
|
||||
when(
|
||||
() => userRepository.update(any(), any()),
|
||||
).thenAnswer((_) async => sampleUser);
|
||||
|
||||
final updated = await controller.update(1, input);
|
||||
|
||||
expect(updated, isNotNull);
|
||||
verify(() => userRepository.update(1, any())).called(1);
|
||||
});
|
||||
|
||||
test('delete 성공', () async {
|
||||
when(() => userRepository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
final success = await controller.delete(1);
|
||||
|
||||
expect(success, isTrue);
|
||||
verify(() => userRepository.delete(1)).called(1);
|
||||
});
|
||||
|
||||
test('restore 성공', () async {
|
||||
when(
|
||||
() => userRepository.restore(any()),
|
||||
).thenAnswer((_) async => sampleUser);
|
||||
|
||||
final restored = await controller.restore(1);
|
||||
|
||||
expect(restored, isNotNull);
|
||||
verify(() => userRepository.restore(1)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
|
||||
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
|
||||
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
||||
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
|
||||
import 'package:superport_v2/features/masters/user/presentation/pages/user_page.dart';
|
||||
|
||||
class _MockUserRepository extends Mock implements UserRepository {}
|
||||
|
||||
class _MockGroupRepository extends Mock implements GroupRepository {}
|
||||
|
||||
class _FakeUserInput extends Fake implements UserInput {}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
return MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeUserInput());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
dotenv.clean();
|
||||
});
|
||||
|
||||
testWidgets('플래그 Off 시 스펙 화면 유지', (tester) async {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_USERS_ENABLED=false\n');
|
||||
|
||||
await tester.pumpWidget(_buildApp(const UserPage()));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('사용자(사원) 관리'), findsOneWidget);
|
||||
expect(find.text('테이블 리스트'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('플래그 On', () {
|
||||
late _MockUserRepository userRepository;
|
||||
late _MockGroupRepository groupRepository;
|
||||
|
||||
setUp(() {
|
||||
dotenv.testLoad(fileInput: 'FEATURE_USERS_ENABLED=true\n');
|
||||
userRepository = _MockUserRepository();
|
||||
groupRepository = _MockGroupRepository();
|
||||
GetIt.I.registerLazySingleton<UserRepository>(() => userRepository);
|
||||
GetIt.I.registerLazySingleton<GroupRepository>(() => groupRepository);
|
||||
|
||||
when(
|
||||
() => groupRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<Group>(
|
||||
items: [Group(id: 1, groupName: '관리자')],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('목록 조회 후 테이블 렌더', (tester) async {
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<UserAccount>(
|
||||
items: [
|
||||
UserAccount(
|
||||
id: 1,
|
||||
employeeNo: 'A001',
|
||||
employeeName: '홍길동',
|
||||
email: 'hong@superport.com',
|
||||
group: UserGroup(id: 1, groupName: '관리자'),
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const UserPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('A001'), findsOneWidget);
|
||||
verify(
|
||||
() => userRepository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: null,
|
||||
groupId: null,
|
||||
isActive: null,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('폼 검증: 필수값 누락', (tester) async {
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<UserAccount>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const UserPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('사번을 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('성명을 입력하세요.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('신규 등록 성공', (tester) async {
|
||||
var listCallCount = 0;
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
listCallCount += 1;
|
||||
if (listCallCount == 1) {
|
||||
return PaginatedResult<UserAccount>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
return PaginatedResult<UserAccount>(
|
||||
items: [
|
||||
UserAccount(
|
||||
id: 2,
|
||||
employeeNo: 'A010',
|
||||
employeeName: '신규 사용자',
|
||||
email: 'new@superport.com',
|
||||
group: UserGroup(id: 1, groupName: '관리자'),
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
);
|
||||
});
|
||||
|
||||
UserInput? capturedInput;
|
||||
when(() => userRepository.create(any())).thenAnswer((invocation) async {
|
||||
capturedInput = invocation.positionalArguments.first as UserInput;
|
||||
return UserAccount(
|
||||
id: 2,
|
||||
employeeNo: capturedInput!.employeeNo,
|
||||
employeeName: capturedInput!.employeeName,
|
||||
group: UserGroup(id: capturedInput!.groupId, groupName: '관리자'),
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(_buildApp(const UserPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialog = find.byType(Dialog);
|
||||
final editableTexts = find.descendant(
|
||||
of: dialog,
|
||||
matching: find.byType(EditableText),
|
||||
);
|
||||
|
||||
await tester.enterText(editableTexts.at(0), 'A010');
|
||||
await tester.enterText(editableTexts.at(1), '신규 사용자');
|
||||
|
||||
await tester.tap(find.text('그룹을 선택하세요'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('관리자'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput?.employeeNo, 'A010');
|
||||
expect(find.byType(Dialog), findsNothing);
|
||||
expect(find.text('A010'), findsOneWidget);
|
||||
verify(() => userRepository.create(any())).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user