| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /test:unit |
| Aliases | /unit:create, /test:usecase |
| Tools | Read, Edit, Write, Glob, Grep |
| Model | sonnet |
| Skills | test |
/test:unit โ ํต์ฌ ๋ก์ง ์๋ ์ ๊ฒ๊ธฐ#
ํ๋ง๋๋ก#
์ฑ์ "๋๋"์ ํด๋นํ๋ ํต์ฌ ๋ก์ง(UseCaseยทRepository)์ด ์ ๋๋ก ๋์ํ๋์ง๋ฅผ ์๋์ผ๋ก ํ์ธํ๋ ๊ฒ์ฌ ์ฝ๋๋ฅผ ๋์ ๋ง๋ค์ด ์ฃผ๋ ๋๊ตฌ์ ๋๋ค. ์ ํ์ ์ถ๊ณ ํ๊ธฐ ์ ์ "๋ฒํผ์ ๋๋ฅด๋ฉด ์ ๋ง ์ผ์ง๋?"๋ฅผ ์๋ ๊ธฐ๊ณ๊ฐ ํ๋ํ๋ ๋๋ฌ ๋ณด๋ฉฐ ํ์ธํด ์ฃผ๋ ๊ฒ๊ณผ ๊ฐ์์.
๋๊ฐยท์ธ์ ์ฐ๋์#
- ์๋ก ๋ง๋ ํต์ฌ ๊ธฐ๋ฅ(๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋ ์ ์ฅํ๋ ๋ถ๋ถ)์ด ์๋๋๋ก ์๋ํ๋์ง ํ์ธํ๊ณ ์ถ์ ๋
- ์ฝ๋๋ฅผ ๊ณ ์น ๋ค, ๋ค๋ฅธ ๊ณณ์ด ๋ง๊ฐ์ง์ง ์์๋์ง ์์ ํ๊ฒ ๊ฒ์ฆํ๊ณ ์ถ์ ๋
-
/test:unit๋ช ๋ น(๋๋ ๋ณ์นญ/unit:create,/test:usecase)์ ์คํํ๋ฉด ์๋์ผ๋ก ์๋ํฉ๋๋ค.
๋ฌด์์ ํด์ฃผ๋์#
-
๊ฒ์ฌ ๋์ ์ฝ๋์ ๋ง๋ ๋จ์ ํ
์คํธ ํ์ผ์ ์๋์ผ๋ก ์์ฑํฉ๋๋ค.
- UseCase ํ
์คํธ:
get_{entity}_usecase_test.dart๊ฐ์ ํ์ผ - Repository ํ
์คํธ:
{feature}_repository_test.dart๊ฐ์ ํ์ผ - ํ
์คํธ์์ ๋ฐ๋ณตํด์ ์ฐ๋ ๊ฐ์ง ๋ฐ์ดํฐ ๋ชจ์(
{feature}_fixture.dart)
- UseCase ํ
์คํธ:
- "์ฑ๊ณตํ์ ๋"๋ฟ ์๋๋ผ "์คํจํ์ ๋๋ ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌ๋๋์ง" ๊น์ง ํจ๊ป ํ์ธํ๋๋ก ๋ง๋ค์ด ์ค๋๋ค.
์ด๋ป๊ฒ ์ฐ๋์#
# ํต์ฌ ๋ก์ง ํ
์คํธ ๋ง๋ค๊ธฐ (๊ธฐ๋ณธ)
/test:unit
# ๋ณ์นญ์ผ๋ก๋ ๊ฐ์ ๋์
/unit:create
/test:usecase
๊ฒ์ฌ ๋์์ ์ง์ ํ ์ ์์ต๋๋ค.
target_class(ํ์): ํ ์คํธํ ํด๋์ค ์ด๋ฆtarget_type(์ ํ):usecase(๊ธฐ๋ณธ๊ฐ) ๋๋repository์ค ์ ํfeature_name(์ ํ): ์ด๋ค ๊ธฐ๋ฅ ๋ชจ๋์ธ์ง ์ด๋ฆ ์ง์
ํ ์คํธ ์ฝ๋๋ฅผ ๋ค ๋ง๋ ๋ค์๋ ์๋ ๋ช ๋ น์ผ๋ก ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ์ค์ ๋ก ํ ์คํธ๋ฅผ ๋๋ ค ๋ด ๋๋ค.
# ๊ฐ์ง ๊ฐ์ฒด(Mock) ์์ฑ
dart run build_runner build --delete-conflicting-outputs
# ํ
์คํธ ์คํ
melos run test:select
์์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์#
- ๊ฒ์ฌํ ์ฝ๋๊ฐ UseCase์ธ์ง Repository์ธ์ง ํ์ ํ๊ณ , ์ ํด์ง ํด๋ ๊ตฌ์กฐ์ ๋ง์ถฐ ํ ์คํธ ํ์ผ์ ๋ ์์น๋ฅผ ์ก์ต๋๋ค.
- ์ง์ง ์ธ๋ถ ์์กด์ฑ(์๋ฒ, ๋ฐ์ดํฐ๋ฒ ์ด์ค) ๋์ ๊ฐ์ง ๋์ญ(Mock) ์ ๋ง๋ค์ด, ์ธ๋ถ ์ํฉ๊ณผ ์๊ด์์ด ๋ก์ง๋ง ๊น๋ํ๊ฒ ๊ฒ์ฌํ ์ ์๊ฒ ์ค๋นํฉ๋๋ค.
- "์ค๋น โ ์คํ โ ๊ฒ์ฆ" 3๋จ๊ณ ํจํด์ผ๋ก ๊ฐ ์ํฉ์ ์ ๊ฒํฉ๋๋ค โ ์ ์ ๋์, ์๋ฒ ์ค๋ฅ, ์๋ชป๋ ์ ๋ ฅ, ์บ์(์์ ์ ์ฅ) ํ์ฉ, ๋๋ฆฐ ์๋ตยท์๊ฐ ์ด๊ณผ ๋ฑ.
- ๋ง๋ ํ ์คํธ๊ฐ ๋น ์ง์์ด ๊ฐ์ถฐ์ก๋์ง ์ฒดํฌ๋ฆฌ์คํธ๋ก ์ ๊ฒํ๊ณ ๋ง๋ฌด๋ฆฌํฉ๋๋ค.
โ๏ธ ์์ธ ์ต์ ยท์คํ ๋ช ์ธ (๊ฐ๋ฐ์ / AI ์์ด์ ํธ์ฉ)
Role#
Generates unit tests for UseCase and Repository.
- Uses @GenerateNiceMocks annotation
- Mockito Pattern (when, verify, verifyNoMoreInteractions)
- Either result verification
- setUp/tearDown patterns
Activation Conditions#
/test:unitActivated when command is invoked- Invoked when writing UseCase and Repository tests
Parameters#
| Parameter | Required | Description |
|---|---|---|
target_class | โ | Test target Class name |
target_type | โ | usecase, repository (default: usecase) |
feature_name | โ | Feature module name |
Test File Structure#
feature/{module_type}/{feature_name}/test/
โโโ src/
โ โโโ unit/
โ โ โโโ usecase/
โ โ โ โโโ get_{entity}_usecase_test.dart
โ โ โ โโโ create_{entity}_usecase_test.dart
โ โ โโโ repository/
โ โ โโโ {feature}_repository_test.dart
โ โโโ fixture/
โ โโโ {feature}_fixture.dart
โโโ {feature}_test.dart # Test entry pointImport Order (Required)#
// 1. Dart test
import 'package:flutter_test/flutter_test.dart';
// 2. Mock package
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
// 3. Dependency packages
import 'package:dependencies/dependencies.dart';
// 4. Test target
import 'package:{feature}/src/domain/usecase/get_{entity}_usecase.dart';
import 'package:{feature}/src/domain/repository/i_{feature}_repository.dart';
// 5. Generated files
import 'get_{entity}_usecase_test.mocks.dart';
Core Patterns#
1. UseCase Test#
import 'package:dependencies/dependencies.dart';
import 'package:feature_home/src/domain/entity/user.dart';
import 'package:feature_home/src/domain/repository/i_home_repository.dart';
import 'package:feature_home/src/domain/usecase/get_user_usecase.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'get_user_usecase_test.mocks.dart';
@GenerateNiceMocks([MockSpec<IHomeRepository>()])
void main() {
late GetUserUseCase useCase;
late MockIHomeRepository mockRepository;
setUp(() {
mockRepository = MockIHomeRepository();
useCase = GetUserUseCase(mockRepository);
});
tearDown(() {
reset(mockRepository);
});
group('GetUserUseCase', () {
const tUserId = 1;
const tUser = User(id: tUserId, name: 'ํ๊ธธ๋', email: 'hong@example.com');
final tParams = GetUserParams(id: tUserId);
test('should return User when repository call is successful', () async {
// Arrange
when(mockRepository.getUser(tUserId))
.thenAnswer((_) async => const Right(tUser));
// Act
final result = await useCase(tParams);
// Assert
expect(result, const Right<Failure, User>(tUser));
verify(mockRepository.getUser(tUserId)).called(1);
verifyNoMoreInteractions(mockRepository);
});
test('should return Failure when repository call fails', () async {
// Arrange
const tFailure = ServerFailure(message: '์๋ฒ ์ค๋ฅ');
when(mockRepository.getUser(tUserId))
.thenAnswer((_) async => const Left(tFailure));
// Act
final result = await useCase(tParams);
// Assert
expect(result, const Left<Failure, User>(tFailure));
verify(mockRepository.getUser(tUserId)).called(1);
verifyNoMoreInteractions(mockRepository);
});
test('should throw when params is invalid', () async {
// Arrange
final invalidParams = GetUserParams(id: -1);
// Act & Assert
expect(
() => useCase(invalidParams),
throwsA(isA<InvalidParamsException>()),
);
verifyZeroInteractions(mockRepository);
});
});
}
2. Repository Test#
import 'package:dependencies/dependencies.dart';
import 'package:feature_home/src/data/repository/home_repository.dart';
import 'package:feature_home/src/domain/entity/user.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'home_repository_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<HomeApiClient>(),
MockSpec<HomeLocalDataSource>(),
])
void main() {
late HomeRepository repository;
late MockHomeApiClient mockApiClient;
late MockHomeLocalDataSource mockLocalDataSource;
setUp(() {
mockApiClient = MockHomeApiClient();
mockLocalDataSource = MockHomeLocalDataSource();
repository = HomeRepository(
apiClient: mockApiClient,
localDataSource: mockLocalDataSource,
);
});
tearDown(() {
reset(mockApiClient);
reset(mockLocalDataSource);
});
group('HomeRepository.getUser', () {
const tUserId = 1;
const tUserDto = UserDto(id: tUserId, name: 'ํ๊ธธ๋', email: 'hong@example.com');
const tUser = User(id: tUserId, name: 'ํ๊ธธ๋', email: 'hong@example.com');
test('should return User when API call is successful', () async {
// Arrange
when(mockApiClient.getUser(tUserId))
.thenAnswer((_) async => tUserDto);
// Act
final result = await repository.getUser(tUserId);
// Assert
expect(result, const Right<Failure, User>(tUser));
verify(mockApiClient.getUser(tUserId)).called(1);
});
test('should cache data locally when API call is successful', () async {
// Arrange
when(mockApiClient.getUser(tUserId))
.thenAnswer((_) async => tUserDto);
when(mockLocalDataSource.cacheUser(any))
.thenAnswer((_) async {});
// Act
await repository.getUser(tUserId);
// Assert
verify(mockLocalDataSource.cacheUser(tUserDto)).called(1);
});
test('should return cached data when API call fails', () async {
// Arrange
when(mockApiClient.getUser(tUserId))
.thenThrow(Exception('Network error'));
when(mockLocalDataSource.getCachedUser(tUserId))
.thenAnswer((_) async => tUserDto);
// Act
final result = await repository.getUser(tUserId);
// Assert
expect(result, const Right<Failure, User>(tUser));
verify(mockLocalDataSource.getCachedUser(tUserId)).called(1);
});
test('should return Failure when both API and cache fail', () async {
// Arrange
when(mockApiClient.getUser(tUserId))
.thenThrow(Exception('Network error'));
when(mockLocalDataSource.getCachedUser(tUserId))
.thenAnswer((_) async => null);
// Act
final result = await repository.getUser(tUserId);
// Assert
expect(result.isLeft(), true);
result.fold(
(failure) => expect(failure, isA<CacheFailure>()),
(_) => fail('Should return Left'),
);
});
});
}
3. Fixture Pattern#
/// Home Feature ํ
์คํธ Fixture
abstract final class HomeFixture {
/// ํ
์คํธ์ฉ User ๊ฐ์ฒด
static const User user = User(
id: 1,
name: 'ํ๊ธธ๋',
email: 'hong@example.com',
createdAt: DateTime(2024, 1, 1),
);
/// ํ
์คํธ์ฉ User ๋ชฉ๋ก
static const List<User> users = [
User(id: 1, name: 'ํ๊ธธ๋', email: 'hong@example.com'),
User(id: 2, name: '๊น์ฒ ์', email: 'kim@example.com'),
User(id: 3, name: '์ด์ํฌ', email: 'lee@example.com'),
];
/// ํ
์คํธ์ฉ UserDto
static const UserDto userDto = UserDto(
id: 1,
name: 'ํ๊ธธ๋',
email: 'hong@example.com',
);
/// ํ
์คํธ์ฉ Failure
static const ServerFailure serverFailure = ServerFailure(
message: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
statusCode: 500,
);
/// ํ
์คํธ์ฉ NetworkFailure
static const NetworkFailure networkFailure = NetworkFailure(
message: '๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์',
);
}
4. Either Result Verification Helpers#
import 'package:dependencies/dependencies.dart';
import 'package:flutter_test/flutter_test.dart';
/// Either ๊ฒฐ๊ณผ ๊ฒ์ฆ ํ์ฅ
extension EitherTestExtension<L, R> on Either<L, R> {
/// Left ๊ฐ ์ถ์ถ (ํ
์คํธ์ฉ)
L getLeft() {
return fold((l) => l, (_) => throw Exception('Expected Left but got Right'));
}
/// Right ๊ฐ ์ถ์ถ (ํ
์คํธ์ฉ)
R getRight() {
return fold((_) => throw Exception('Expected Right but got Left'), (r) => r);
}
}
/// Either ๋งค์ฒ
Matcher isRightWith<R>(R expected) {
return predicate<Either<dynamic, R>>(
(either) => either.fold((_) => false, (r) => r == expected),
'is Right with $expected',
);
}
Matcher isLeftWith<L>(L expected) {
return predicate<Either<L, dynamic>>(
(either) => either.fold((l) => l == expected, (_) => false),
'is Left with $expected',
);
}
Matcher isLeftOfType<L>() {
return predicate<Either<L, dynamic>>(
(either) => either.fold((l) => l is L, (_) => false),
'is Left of type $L',
);
}
5. Async Test Patterns#
group('async operations', () {
test('should handle async operation correctly', () async {
// Arrange
when(mockRepository.fetchData())
.thenAnswer((_) async {
await Future.delayed(const Duration(milliseconds: 100));
return const Right(data);
});
// Act
final future = useCase();
// Assert
await expectLater(future, completes);
final result = await future;
expect(result.isRight(), true);
});
test('should timeout when operation takes too long', () async {
// Arrange
when(mockRepository.fetchData())
.thenAnswer((_) async {
await Future.delayed(const Duration(seconds: 10));
return const Right(data);
});
// Act & Assert
await expectLater(
useCase().timeout(const Duration(seconds: 1)),
throwsA(isA<TimeoutException>()),
);
});
});
Mockito Pattern Summary#
| Method | Purpose | Example |
|---|---|---|
when(...).thenReturn() | Set synchronous return value | when(mock.getValue()).thenReturn(42) |
when(...).thenAnswer() | Set async return value | when(mock.getData()).thenAnswer((_) async => data) |
when(...).thenThrow() | Set exception throwing | when(mock.call()).thenThrow(Exception()) |
verify(...).called(n) | Verify call count | verify(mock.call()).called(1) |
verifyNever(...) | Verify not called | verifyNever(mock.call()) |
verifyNoMoreInteractions(...) | Verify no additional calls | verifyNoMoreInteractions(mock) |
verifyZeroInteractions(...) | Verify zero calls | verifyZeroInteractions(mock) |
reset(...) | Reset mock state | reset(mock) |
any | Match all values | when(mock.call(any)).thenReturn(true) |
argThat(...) | Conditional matching | argThat(isA<String>()) |
captureAny | Argument capture | verify(mock.call(captureAny)) |
Build Commands#
# Mock ์์ฑ
cd feature/{module_type}/{feature_name}
dart run build_runner build --delete-conflicting-outputs
# Tests ์คํ
melos run test:select
# Test with coverage
melos run test:with-html-coverageReference Files#
feature/application/store/test/src/unit/usecase/
feature/application/store/test/src/unit/repository/
feature/common/auth/test/src/unit/Checklist#
- Add @GenerateNiceMocks Annotation
- Generate mock classes (.mocks.dart)
- Apply setUp/tearDown Pattern
- Group tests with group
- Apply Arrange-Act-Assert Pattern
- Test both success/failure cases
- Verify calls with verify
- Verify Either results
- Utilize Fixtures