[Flutter] 클린 아키텍처에서 유스케이스 테스트하기 (AAA 패턴)

도톨이·2025년 4월 7일
0

앱 개발-flutter

목록 보기
32/34

이번에는 지난번 작성한 유스케이스(CreateUser)의 테스트 코드를 작성해보며, 테스트 코드의 구조인 AAA 패턴mock 객체(Mocktail) 사용법을 정리해보려 한다.

참고를 위한 유스케이스 코드 (create_user.dart)

class CreateUser extends UsecaseWithParams<void, CreateUserParams> {
  const CreateUser(this._repository);

  final AuthenticationRepository _repository;

  
  ResultVoid call(CreateUserParams params) async =>
      _repository.createUser(
        createdAt: params.createdAt,
        name: params.name,
        avatar: params.avatar,
      );
}

class CreateUserParams extends Equatable {
  final String createdAt;
  final String name;
  final String avatar;

  const CreateUserParams.empty() :
        this(createdAt: '_empty.createdAt', name: '_empty.name', avatar: '_empty.avatar');

  const CreateUserParams({
    required this.createdAt,
    required this.name,
    required this.avatar,
  });

  
  List<Object?> get props => [createdAt, name, avatar];
}

들어가기에 앞서,
플러터 프로젝트에서 테스트 코드를 작성하기 위해 유용한 플러그인 Dart Test 을 설치한다.

이걸 사용하면 테스트하고자 하는 파일을 우클릭하여 Create Dart test file 을 클릭하면 테스트 폴더 내에 테스트 파일을 손쉽게 생성할 수 있다.

Unit Test 시작하기

테스트 코드 작성을 위해 Mocktail 패키지를 설치한다.
모킹을 위해 Mocktail 또는 Mockito 을 사용할 수 있다.
MocktailMockito는 둘 다 Dart/Flutter에서 테스트용 가짜 객체(mock)를 만드는(=모킹하는) 라이브러리이다.

MocktailMochkito 보다 더 간단하다. Mockito 는 build_runner 을 써서 코드 생성이 필요하지만, Mocktail 은 코드 생성 없이 바로 쓸 수 있어서 요즘엔 Mocktail을 더 많이 사용한다.

터미널에 아래의 명령어를 입력해서 설치해준다.

flutter pub add -d mocktail && flutter pub get

🤖 왜 Mock 객체를 사용할까?

  • 오늘 필자가 테스트하고자 하는 것은 유스케이스 그 자체로 Repository의 실제 동작은 테스트 대상이 아니다.
  • 만약 Repository가 내부적으로 네트워크 요청이나 DB 접근 등 외부 환경에 의존하고 있다면, 이 테스트는 느려지고 불안정해질 수도 있다.
  • 따라서 Repository를 가짜(Mock)로 대체하여, 유스케이스 로직만 순수하게 검증할 수 있도록 한다.

🧱 AAA 패턴이란?

테스트 코드는 AAA 패턴(Arrange - Act - Assert)에 따라 작성하는 것이 일반적이다.
이 구조는 테스트의 흐름을 명확하게 하고, 가독성과 유지보수성을 높여줄 수 있다.

단계의미설명
Arrange준비테스트에 필요한 Mock, Stub 등 설정
Act실행테스트 대상 메서드 또는 클래스 실행
Assert검증기대한 결과가 실제와 일치하는지 확인

테스트 코드 작성

해당 구조를 지키며 테스트 코드를 다음처럼 작성하였다(전체 코드)

class MockAuthRepo extends Mock implements AuthenticationRepository {}

void main() {
  late CreateUser usecase;
  late AuthenticationRepository repository;

  setUpAll(() {
    repository = MockAuthRepo();
    usecase = CreateUser(repository);
  });

  final params = CreateUserParams.empty();

  test('should call the [Repository.createUser]', () async {
    // Arrange
    when(
      () => repository.createUser(
        createdAt: any(named: 'createdAt'),
        name: any(named: 'name'),
        avatar: any(named: 'avatar'),
      ),
    ).thenAnswer((_) async => const Right(null));

    // Act
    final result = await usecase(params);

    // Assert
    expect(result, equals(const Right<Failure, void>(null)));
    verify(
      () => repository.createUser(
        createdAt: params.createdAt,
        name: params.name,
        avatar: params.avatar,
      ),
    ).called(1);
    verifyNoMoreInteractions(repository);
  });
}

우선, 리포지토리의 모킹 클래스를 정의해야한다. mocktail 패키지에서 제공하는 Mock 클래스를 상속하여, 우리가 테스트할 Repository의 가짜 버전을 만들 수 있다. 이는 실제 AuthenticationRepository를 구현할 필요 없이, 테스트용으로만 사용하는 가벼운 객체이다.

class MockAuthRepo extends Mock implements AuthenticationRepository {}

다음으로는 초기화이다. setUpAll 테스트 전체에서 한 번만 실행되는 초기화 블록이다.
여기서 테스트 대상인 CreateUser는 실제 Repository 대신 MockAuthRepo를 주입받으면 된다. (의존성 주입).

setUpAll(() {
  repository = MockAuthRepo();
  usecase = CreateUser(repository);
});

파라미터도 정의해준다. 해당 파라미터는 CreateUserParams 에 비어있는 객체를 생성하는 메서드를 따로 만들어줬다.

final params = CreateUserParams.empty();

create_user.dart

  const CreateUserParams.empty() :
        this(createdAt: '_empty.createdAt', name: '_empty.name', avatar: '_empty.avatar');

1. Arrange

이제 Arrange 과정으로 Stubbing(행동 정의) 을 진행한다.

  • when(...).thenAnswer(...)를 통해 가짜 Repository가 어떻게 행동할지를 지정한다.
  • any(named: ...)는 매개변수 값에 상관없이 허용한다는 의미이다.
  • Right(null)은 성공적인 결과를 의미한다. (Either<Failure, void>).
when(
  () => repository.createUser(
    createdAt: any(named: 'createdAt'),
    name: any(named: 'name'),
    avatar: any(named: 'avatar'),
  ),
).thenAnswer((_) async => const Right(null));

정리하자면 when(...).thenAnswer(...) 을 통해 "만약에 repository.createUser(...)가 호출된다면, 이렇게 응답해!" 라고 설정할 수 있다.

인자에 있는 any(named: '...') 는 특정 값이 들어오지 않아도 괜찮다는 뜻으로 테스트에서 값 자체가 중요한 게 아니라 "호출되었는지 여부" 가 중요하니까, 어떤 값이 들어오든 허용한다는 뜻이다. thenAnswer((_) async => const Right(null)) 을 통해 이 메서드가 실제로 호출되면 성공을 의미하는 값(Right) 을 비동기적으로 돌려준다.
Right(null)은 "성공했고, 별도의 리턴값은 없다"는 뜻.
(Either<Failure, void> 구조에서 성공 쪽이 Right)

2. Act

다음으로 실제 테스트 대상(유스케이스)을 실행한다 (Act)

final result = await usecase(params);

3. Assert

마지막으로 결과 검증 및 호출 여부를 확인한다. (Assert)

expect(result, equals(const Right<Failure, void>(null)));

결과가 예상한 성공 결과와 같은지 검증한다.
called(1) 을 통해 createUser가 정확히 한 번 호출되었는지 확인할 수 있다.

verify(() => repository.createUser(...)).called(1);

그리고, verifyNoMoreInteractions을 통해 더 이상 다른 메서드가 호출되지 않았는지도 검증한다.

verifyNoMoreInteractions(repository);

테스트 결과

테스트는 Run 'tests in create_user` 을 눌러서 테스트할 수 있다.

테스트 결과는 다음처럼 Tests passed: 1 으로 확인할 수 있다.

profile
Kotlin, Flutter, AI | Computer Science

0개의 댓글