LogoSkills

unit-test-agent

UseCase and Repository unit test specialist. Used for Mockito patterns and Either result verification

ํ•ญ๋ชฉ๋‚ด์šฉ
Invoke/test:unit
Aliases/unit:create, /test:usecase
ToolsRead, Edit, Write, Glob, Grep
Modelsonnet
Skillstest

/test:unit โ€” ํ•ต์‹ฌ ๋กœ์ง ์ž๋™ ์ ๊ฒ€๊ธฐ#

ํ•œ๋งˆ๋””๋กœ#

์•ฑ์˜ "๋‘๋‡Œ"์— ํ•ด๋‹นํ•˜๋Š” ํ•ต์‹ฌ ๋กœ์ง(UseCaseยทRepository)์ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€๋ฅผ ์ž๋™์œผ๋กœ ํ™•์ธํ•˜๋Š” ๊ฒ€์‚ฌ ์ฝ”๋“œ๋ฅผ ๋Œ€์‹  ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ œํ’ˆ์„ ์ถœ๊ณ ํ•˜๊ธฐ ์ „์— "๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ •๋ง ์ผœ์ง€๋‚˜?"๋ฅผ ์ž๋™ ๊ธฐ๊ณ„๊ฐ€ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ˆŒ๋Ÿฌ ๋ณด๋ฉฐ ํ™•์ธํ•ด ์ฃผ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์•„์š”.

๋ˆ„๊ฐ€ยท์–ธ์ œ ์“ฐ๋‚˜์š”#

  • ์ƒˆ๋กœ ๋งŒ๋“  ํ•ต์‹ฌ ๊ธฐ๋Šฅ(๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ์ €์žฅํ•˜๋Š” ๋ถ€๋ถ„)์ด ์˜๋„๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์‹ถ์„ ๋•Œ
  • ์ฝ”๋“œ๋ฅผ ๊ณ ์นœ ๋’ค, ๋‹ค๋ฅธ ๊ณณ์ด ๋ง๊ฐ€์ง€์ง€ ์•Š์•˜๋Š”์ง€ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์„ ๋•Œ
  • /test:unit ๋ช…๋ น(๋˜๋Š” ๋ณ„์นญ /unit:create, /test:usecase)์„ ์‹คํ–‰ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”#

  • ๊ฒ€์‚ฌ ๋Œ€์ƒ ์ฝ”๋“œ์— ๋งž๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • UseCase ํ…Œ์ŠคํŠธ: get_{entity}_usecase_test.dart ๊ฐ™์€ ํŒŒ์ผ
    • Repository ํ…Œ์ŠคํŠธ: {feature}_repository_test.dart ๊ฐ™์€ ํŒŒ์ผ
    • ํ…Œ์ŠคํŠธ์—์„œ ๋ฐ˜๋ณตํ•ด์„œ ์“ฐ๋Š” ๊ฐ€์งœ ๋ฐ์ดํ„ฐ ๋ชจ์Œ({feature}_fixture.dart)
  • "์„ฑ๊ณตํ–ˆ์„ ๋•Œ"๋ฟ ์•„๋‹ˆ๋ผ "์‹คํŒจํ–ˆ์„ ๋•Œ๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€" ๊นŒ์ง€ ํ•จ๊ป˜ ํ™•์ธํ•˜๋„๋ก ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ ์“ฐ๋‚˜์š”#

# ํ•ต์‹ฌ ๋กœ์ง ํ…Œ์ŠคํŠธ ๋งŒ๋“ค๊ธฐ (๊ธฐ๋ณธ)
/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

์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋‚˜์š”#

  1. ๊ฒ€์‚ฌํ•  ์ฝ”๋“œ๊ฐ€ UseCase์ธ์ง€ Repository์ธ์ง€ ํŒŒ์•…ํ•˜๊ณ , ์ •ํ•ด์ง„ ํด๋” ๊ตฌ์กฐ์— ๋งž์ถฐ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ๋‘˜ ์œ„์น˜๋ฅผ ์žก์Šต๋‹ˆ๋‹ค.
  2. ์ง„์งœ ์™ธ๋ถ€ ์˜์กด์„ฑ(์„œ๋ฒ„, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) ๋Œ€์‹  ๊ฐ€์งœ ๋Œ€์—ญ(Mock) ์„ ๋งŒ๋“ค์–ด, ์™ธ๋ถ€ ์ƒํ™ฉ๊ณผ ์ƒ๊ด€์—†์ด ๋กœ์ง๋งŒ ๊น”๋”ํ•˜๊ฒŒ ๊ฒ€์‚ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.
  3. "์ค€๋น„ โ†’ ์‹คํ–‰ โ†’ ๊ฒ€์ฆ" 3๋‹จ๊ณ„ ํŒจํ„ด์œผ๋กœ ๊ฐ ์ƒํ™ฉ์„ ์ ๊ฒ€ํ•ฉ๋‹ˆ๋‹ค โ€” ์ •์ƒ ๋™์ž‘, ์„œ๋ฒ„ ์˜ค๋ฅ˜, ์ž˜๋ชป๋œ ์ž…๋ ฅ, ์บ์‹œ(์ž„์‹œ ์ €์žฅ) ํ™œ์šฉ, ๋А๋ฆฐ ์‘๋‹ตยท์‹œ๊ฐ„ ์ดˆ๊ณผ ๋“ฑ.
  4. ๋งŒ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ๋น ์ง์—†์ด ๊ฐ–์ถฐ์กŒ๋Š”์ง€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ์ ๊ฒ€ํ•˜๊ณ  ๋งˆ๋ฌด๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

โš™๏ธ ์ƒ์„ธ ์˜ต์…˜ยท์‹คํ–‰ ๋ช…์„ธ (๊ฐœ๋ฐœ์ž / 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:unit Activated when command is invoked
  • Invoked when writing UseCase and Repository tests

Parameters#

ParameterRequiredDescription
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 point

Import 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#

MethodPurposeExample
when(...).thenReturn()Set synchronous return valuewhen(mock.getValue()).thenReturn(42)
when(...).thenAnswer()Set async return valuewhen(mock.getData()).thenAnswer((_) async => data)
when(...).thenThrow()Set exception throwingwhen(mock.call()).thenThrow(Exception())
verify(...).called(n)Verify call countverify(mock.call()).called(1)
verifyNever(...)Verify not calledverifyNever(mock.call())
verifyNoMoreInteractions(...)Verify no additional callsverifyNoMoreInteractions(mock)
verifyZeroInteractions(...)Verify zero callsverifyZeroInteractions(mock)
reset(...)Reset mock statereset(mock)
anyMatch all valueswhen(mock.call(any)).thenReturn(true)
argThat(...)Conditional matchingargThat(isA<String>())
captureAnyArgument captureverify(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-coverage

Reference 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