현재 진행중인 SoloPlay! 프로젝트는 TDD를 채택하여 테스트로 코드의 설계와 품질 검증을 동시에 진행하고 있습니다. 추가로 골든 테스트도 함께 적용하고 있는데요. 오늘은 골든테스트 그리고 도입 배경과 실질적인 개선 효과를 공유하고자 합니다.
위젯 테스트는 제작한 위젯이 올바른지 검증할 수 있는 훌륭한 테스트입니다. 하지만 위젯 테스트만으로 UI의 요구사항을 모두 검증하는 것은 굉장히 리소스가 많이 소요됩니다. 모든 제약조건과 정렬을 검증한다고 하면 수없이 많은 테스트 코드가 발생할 것입니다. 이 경우 Golden Test가 굉장히 유용할 수 있습니다.
Flutter는 Golden Test를 제공합니다. Golden Test는 실질적으로 Snapshot Test와 비슷한 의미를 갖고 있습니다.
Golden Test이라는 이름은 영화산업에서 유래된 이름입니다. "Golden Reel"이 최종 승인된 영화의 버전을 의미한다는데요. Golden Test에서도 Golden은 최종 승인된 UI를 의미하게 됩니다.
예를 통해 골든 테스트에 대해서 알아보겠습니다. 아래는 현재 프로젝트에서 필요한 회원가입 UI 중 일부입니다.
초기 화면에는 이메일을 입력받을 수 있으며, 이메일 형식이 올바르다면 버튼이 활성화됩니다. 만약 어떠한 에러가 발생한다면 Validation 메시지가 보이며 버튼이 활성화되지 않습니다.
우리는 이러한 코드를 각각 TDD로 구현하면서 실제로 어떻게 UI에 나타나는지 확인하기는 어렵습니다. 왜냐하면 테스트는 결국 결과만을 보여주기에 아래와 같은 결과만 보여줍니다.
하지만 여기서 각 상황에 따른 골든 테스트를 적용한다면 어떨까요?
...
void main() {
...
group(RegisterEmailUI, () {
late MockUserEmailBloc mockUserEmailBloc;
late Widget widget;
setUp(() {
// 테스트 설정
});
...
// 초기 UI 검증
testWidgets("should render default UI correctly", (tester) async {
await tester.pumpWidget(widget);
await expectLater(
find.byType(RegisterEmailUI),
matchesGoldenFile('goldens/register-email-ui-default.png'),
);
});
testWidgets("button should be enabled when status is valid",
(tester) async {
when(() => mockUserEmailBloc.state).thenReturn(const UserEmailState(
email: "test@test.com", status: UserEmailStatus.valid));
await tester.pumpWidget(widget);
final button = tester.widget<NextStepButton>(find.byType(NextStepButton));
expect(button.onTap, isNotNull);
expectLater(find.byType(RegisterEmailUI),
matchesGoldenFile('goldens/register-email-ui-valid.png'));
});
testWidgets("should render error state correctly when email is duplicated",
(tester) async {
when(() => mockUserEmailBloc.state).thenReturn(
const UserEmailState(
email: "duplicate@test.com",
status: UserEmailStatus.conflict,
errorMessage: "이메일이 이미 존재합니다",
),
);
await tester.pumpWidget(widget);
await tester.pump(); // Ensure all widgets are rebuilt
// 1. PrimaryTextField의 isError가 활성화되어야
final primaryTextField =
tester.widget<PrimaryTextField>(find.byType(PrimaryTextField));
expect(primaryTextField.isError, isTrue);
// 2. RegisterEmailValidateTextView에 에러가 보여야 하며
expect(find.text("이메일이 이미 존재합니다"), findsOneWidget);
// 3. RegisterEmailValidateIconView는 isValid가 false가 전달되어야 함
final validationIcon =
tester.widget<ValidationIcon>(find.byType(ValidationIcon));
expect(validationIcon.isValid, isFalse);
// 4. 골든테스트 실제 UI가 맞는지 확인
await expectLater(find.byType(RegisterEmailUI),
matchesGoldenFile('goldens/register-email-ui-conflict.png'));
});
});
}
위 코드는 실제로 제작된 테스트 코드 중 일부인데요. 각각 초기 UI, 그리고 올바른 형식의 이메일인 시나리오의 UI, 에러가 발생한 시나리오의 UI까지 3가지 경우를 검증합니다.
잠시 Golden Test를 수행하는 코드에 대해서 알아봅시다.
testWidgets("test description",() {
await tester.pumpWidget(widget);
// 골든 테스트
expectLater(find.byType(WidgetType),
matchesGoldenFile('[golden file path]'));
});
Golden Test는 기본적인 라이브러리에서 이미 제공합니다. Finder를 통해 찾은 위젯에 대한 스냅샷과 기존 스냅샷을 matchesGoldenFile 메소드로 수행할 수 있습니다. 기존 스냅샷의 위치는 matchesGoldenFile 메소드의 파라미터로 전달하면 됩니다.
Golden Test는 처음 생성한 경우 Snapshot이 존재하지 않기에 일반적인 테스트 명령으로는 테스트가 실패합니다. 때문에 --update-goldens 라는 옵션과 함께 사용하면 초기 Snapshot을 생성할 수 있습니다.
$ flutter test --update-goldens
이렇게 생성된 골든 파일은 matchesGoldenFile메소드 파라미터 해당 경로에 생성됩니다.
- 기본 UI
- 올바른 이메일 형식인 경우
- 에러가 발생한 경우
여기서, 텍스트는 네모난 상자로 보이게 되는데요. 골든 테스트는 텍스트를 포함한 위젯을 이미지로 렌더링합니다. 이때 사용되는 폰트가 테스트 환경에 존재하지 않거나, 테스트 렌더링 엔진이 해당 폰트를 제대로 인식하지 못하면 글자가 깨져 보이거나 네모난 상자로 표시됩니다.
저희팀 같은 경우 시뮬레이터 환경에서 UI를 제작하는 경우가 굉장히 잦았기에 앱 진입점 변경이 수없이 이루어졌고, 이는 곧 Git에서 병합충돌로 이어졌습니다.
하지만 Golden Test 도입 이후로는 시뮬레이터를 실행하지 않아도 UI제작이 가능해져 병합 충돌 발생 빈도도 50% 이상 감소할 수 있었습니다.또한, Golden Test를 수행하면서 직접 눈으로 완성된 UI를 확인할 수 있고, 디자이너와 빠르게 소통할 수 있었습니다. 무엇보다도 작업 중 의도치 않은 UI 요구사항이 변경되더라도 조기 파악이 가능했습니다.
외에도 Golden Test를 여러 디바이스의 반응형 UI 검증에서도 사용할 수 있습니다.
하지만 장점만큼 단점도 존재합니다.
- 초기 설정에 시간이 걸림
- 작은 UI 변경에도 테스트가 실패할 수 있음
- 다른 운영 체제나 환경에서 결과가 다를 수 있음
- 골든 이미지 관리가 번거로움
실제로 저희팀 같은 경우 Golden Test는 테스트 한줄이 추가된 것에서 더이상의 영향은 없었지만 반응형 UI 검증이나 다른 경우에는 초기 설정이 충분히 오래 걸릴 수 있습니다.
또한, CI/CD 환경에서와 로컬 작업 환경의 OS로 인해 Golden Test가 실패할 수 있기에 별도의 해결책을 마련해야 합니다. 저희팀은 모든 인원이 MacOS를 이용하기에 CI/CD OS를 MacOS로 변경하여 해결했습니다. 그러나, 그렇지 않은 경우에는 오차 범위를 지정하여 해결할 수 있습니다.