About Dr.diary
Home
💻

UI테스트 자동화 적용기

이 아티클은 2022. 11. 5. 에 진행한 Let us:Go! 2022 fall 밋업에서 발표한 주제입니다.

안녕하세요~ 닥터다이어리 iOS 개발자 한승진 입니다.
제가 닥터다이어리에 합류한 직후, 경영진의 배려로 프로젝트를 재정비할 수 있는 기회가 있었습니다. 개발팀은 그 기회에 CICD 배포 자동화 파이프라인 구축과 함께, 이를 더욱 극대화해줄 테스트 자동화까지 구축해 보기로 담대한 계획을 세웠습니다. 그리고 그 계획은 확실히 짧지 않은 기간, 많은 노력과 시행착오를 거쳐 완성되었습니다.
덕분에 닥터다이어리 앱(이하 ‘닥다앱’)에 신규 기능을 부지런히 추가할 때도 사이드이펙트에 부담을 덜고, 품질 유지를 위한 서비스 개발 조직 전체의 리소스를 절감할 수 있었습니다.
이 아티클을 통해 UI 테스트를 적용하며 겪은 경험을 재밌게 읽어주시되, 각자의 상황에 맞춰 유연하게 활용한(해야 할) 부분이 많다는 사실도 함께 알아주세요.

주제 빌드 업

Unit테스트와는 다른 UI테스트!
UI테스트, 조금만 더 살펴보기
UI테스트, 장점이 뭐냐면요?
UI테스트에 자동화 적용하기

Unit테스트와는 다른 UI테스트!

테스트란 단어로 인해 우리가 익숙히 알고 있는 유닛테스트와 그 개념을 혼동할 수 있습니다. 소프트웨어 테스트에는 여러 종류가 존재하며 UI 테스트 역시 유닛테스트와 같이 한 종류의 테스트입니다.
UI테스트는 유닛 테스트, 통합 테스트 등 보다 대형 테스트 기법으로 분류되며, 실제 앱을 사용자의 흐름에 따라 화면에 직접 터치해가며 일련의 동작을 수행해 보는 테스트입니다. 말 그대로 User Interface Test 인 것이죠.
그에 더해 UI테스트 자동화는 UI테스트를 일정 주기 혹은 브랜치 병합 등의 특정 이벤트로 자동화하여 수행하는 것을 말합니다.
Unit테스트와 UI테스트의 미묘한 뉘앙스 차이를 코드로 간단히 첨부한다면 이해하는데 좋을 것 같네요!
Unit테스트) API Response Code 를 검사합니다.
UI테스트) UI 요소들의 상태들을 검사합니다.
대표적인 테스트의 종류에 대해 미리 알아두면 이 글을 읽는 데 도움이 되므로 아래 문단 블록에 간단한 요약을 남겨 두겠습니다.
유닛 테스트 - 작은(함수 단위)의 기능에 대한 유효성을 검증하는 테스트 통합 테스트 - 서로 다른 모듈 혹은 클래스 간 상호작용의 유효성을 검사하는 테스트 UI테스트 - 화면에 대한 테스트를 하여 예상되는 기능이 작동하는지 검증하는 테스트

UI 테스트, 조금 더 살펴보기

먼저, 이 아티클의 주제인 UI테스트와 그 자동화에 대하여 경험이 없는 사람은 대다수일 것이므로, 테스트 코드의 일부 조각과 간단한 실행 영상을 첨부해 여러분들의 흥미를 이끌어 봅니다!
로그인과 회원가입 기능은 어느 서비스에서든 핵심 기능입니다.
이런 핵심 기능의 동작 테스트를 실제 유저 플로 대로 직접 눌러가며 검증할 수 있다면, 노력이 조금 필요하더라도 분명 남는 장사입니다. 개발자라면 으레 반복되는 작업은 자동화해야 하니까요! 심지어 Xcode에서 프로젝트를 생성하면 UITest 프로젝트도 자동으로 생성됩니다. UI테스트가 그렇게 멀리 있는 게 아니죠!
왼편 샘플 영상의 동작이 복잡해 보일지라도 UI테스트의 코드 구현은 참 쉽습니다. 첨부한 샘플 코드를 통해서 볼 수 있듯이 코드도 직관적인 편입니다.
위 샘플 UI테스트 코드의 구성은, 2~3 line: 테스트할 화면으로 이동하여 테스트 준비 5~9 line: 유저가 수행할 동작을 코드로 수행 11~13 line: UI 동작 결과 검증
개인적으로 given-when-then 와 함께 핵심 기능의 동작 시나리오(워크플로)를 함께 고려하여 테스트 코드를 작성하길 추천합니다.
이 아티클의 주요 초점은 UI테스트 구현의 자세한 설명보다는 UI테스트 도입의 이점 입니다. 다만, 유용한 팁과 트러블 슈팅 경험도 한 가지 함께 소개합니다.
1.
app.launchArguments 를 통해 유용한 옵션을 설정합니다.
app.launchArguments = ["testing"] // testing 동안 애니메이션을 모두 비활성화 app.launchArguments = [ "-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXXL" ] // testing 동안 폰트가 가장 큰 크기로 실행됨
Swift
복사
2.
필요에따라 LLDB 디버거 실행 끄기
닥다 앱은 프로젝트의 규모가 제법 큰 편이라, LLDB 실행까지 수초 이상의 딜레이가 생깁니다. 직접 브레이크를 걸고 코딩하는 상황이 아닌 한 디버거를 실행하지 않는 게 정신건강에 이롭습니다. 그 결과 테스트 코드 개발 속도와 UI테스트 전체 동작 시간 절약, 시뮬레이터 딜레이로 인한 간헐적인 테스트 실패를 방지할 수 있습니다.

UI테스트, 장점이 뭐냐면요?

물론, 저 또한 UI 테스트는 생소한 개념이었습니다. 당장 도입을 준비할 때까지도 그저 나중에 한번 해보긴 해야 하는데.. 딱 이 정도로 외면해왔던 분야였죠. 저를 제외하고도 다른 분들 역시 그러실 거라 생각합니다. (헉! 아니라고요? )
또한 UI테스트 적용을 준비하며 예상되는 비용이 높았습니다. 가장 먼저 예상되던 신기술 도입의 러닝 커브를 제외하고도요.
실제로 완성된 UI테스트의 1회 실행 테스트 러닝타임은 30분에 육박할 정도로 오래 걸립니다 CI Agent는 iOS 시뮬레이터 혹은 USB 포트로 연결된 아이폰이 항시 준비되어 있어야 합니다.
심지어 테스트가 때때로 불안정해지고 화면과 기획이 변경되면 해당 화면의 테스트 코드는 수명이 다 합니다. 코드가 금방 낡아버리는 만큼 테스트 코드 유지에 필요한 노오오력 역시 비싼 비용입니다.
그래도 참아야한다!

첫 번째 목적은 앱(혹은 개발자 심리) 안정성 확보에 있습니다.

개발자들은 항상 바쁘게 무언가를 만들기에, 새로운 기능의 "추가”와 또, 지난 기능의 “삭제”가 빈번합니다.
예를 들어, 당신이 더 이상 사용하지 않는 이미지 리소스를 삭제한 경우를 상상해 봅시다. 그런데 만약 누군가 그 이미지를 다른 화면에서도 같이 사용하도록 추가해놓은 상황이라면? 그 이미지는 어느 순간부터 (심지어 아무도 모르게) 출력되지 않는 이미지가 되어버릴 겁니다.
예시로 든 이미지 리소스를 라이브러리 / swift 버전 / iOS 버전 업데이트 등으로 확장하면 상황은 더 끔찍해집니다.
이처럼 시시각각 변화하는 제품에 대해 예상할 수 없는 ‘사이드이펙트’를 테스트 자동화로 제어하는 데 의미를 갖습니다. 특히 UI 테스트는 유저에게 직접 와닿는 미동작, 강제 종료 등 치명적인 버그를 발견하는데 더욱 효과적입니다.

두 번째 목적은 조직의 비용(시간+인력) 절감에 목적을 둡니다.

사실 닥다 앱은 꽤나 많은 레거시 코드를 보유하고 있었습니다. 저희 iOS 개발팀은 유지 보수의 한계를 만난 레거시 코드에 대해 점진적인 리팩토링을 하기로 약속했습니다. 만약 기능 개발을 진행하다가 레거시 코드를 만난다면 곧바로 그 부분에 제한적인 개선을 진행합니다. 때때로 레거시 코드로부터 비즈니스 로직을 해석해 세련된 아키텍처로 레코드하는 대규모 리팩토링 작업을 진행합니다. 슬프게도 이러한 리팩토링 작업은 꽤나 잦은 빈도로 진행됩니다.
레거시코드 (Legacy Code) 코드의 가독성이 떨어지거나, 히스토리를 알 수 없는 등 여러 가지 원인들로 관리되지 않는 코드
불행하게도, ‘한정적인’ 리팩토링일지라도 그 결과는 예측되지 않습니다. 리팩토링의 작업의 변경 범위와 코드 실행 결과는 서로 별개의 것이었습니다. 레거시 코드의 변경은 높은 확률로 사이드이펙트를 생성해냅니다. 이에 대한 심리적/시간적 부담감 모두 코드 담당자의 몫입니다.
리팩토링의 완성은 작업한 부분과 그 외 앱 전반적인 동작에 있어 아무런 영향을 끼치지 않는지 직접 확인해 보는 것으로 마무리됩니다. 개발팀은 이때마다 QA 팀에 회귀 테스트를 부탁해야 했고, 부탁하는 쪽도 받는 쪽도 모두 큰 부담입니다. QA 팀의 리소스 역시 한정된 자원이고, 특히나 리팩토링 영향도 점검에 가장 효과적인 회귀 테스트는 매우 매우! 비싼 테스트니까요.
이대로 상황이 흘러간다면 개발자도, QA 팀도 점진적인 개선을 위한 리팩토링을 미워하게 될 겁니다. (iOS 앱 또 리팩토링하셨어요?) 어쩌면 이 점이 UI테스트를 도입하게 된 메인 아이디어 였습니다. 위기와 불편은 항상 극복하는 맛이 있죠. UI테스트의 적용은 개발팀의 리팩토링을 검증해 줄 수 있는 UI통합테스트이자, 자동화를 적용하여 UI스모크 테스트의 역할도 기대할 수 있었습니다.
회귀 테스트 (a.k.a 리그레션 테스트, Regression Test) - 이미 테스트된 부분을 재 검수함. 결함 수정 이후 변경의 결과로 새롭게 만들어진 또 다른 결함을 발견 스모크 테스트(Smoke Test) - 본격적인 테스트에 앞서 테스트가 가능한지 판단하기 위해 시스템을 간단하게 테스트하는 것

UI테스트에 자동화 적용하기

UI테스트에 자동화의 적용은 적절합니다. 아니, 아주 옳았습니다. QA의 수동 테스트와는 달리 UI테스트는 원하는 언제든, 몇 번이고 반복 실행도 부담 없이 가능합니다.
저희는 UI테스트를 CI/CD의 파이프라인에 적절히 적용해 주어 자동화 테스트 환경을 구축했습니다. 한 가지 참고할 사항은 전통적인 의미의 CI/CD가 아닌, 커스터마이징 사항을 두었단 점입니다. 대표적으로 UI테스트 실행에 대한 트리거를 CI/CD 파이프라인에 연결시키지 않은 것입니다.
CI/CD - 지속적인 통합(Continuous Integration) / 지속적인 배포(Continuous Deployment) 간단히 빌드, 테스트의 자동화 와 배포 자동화.
사실 UI테스트는 그 결과가 그리 안정적이지 않습니다. 우리 애증의 Xcode에서는 하루에도 몇 번이나 빌드가 멈추고, 시뮬레이터와의 연결이 끊어지기에 UI테스트도 이를 피해 가지는 못합니다. 심지어 맥 기기의 성능이 그다지 좋지 않다는 이유로, 또는 테스트할 화면의 UI 컴포넌트가 조금 복잡하다는 이유로 테스트는 절반에 가까운 높은 확률로 실패해버립니다. 반반 치킨도 아니고.
이는 분명히 짚고 넘어가야 할 문제였습니다. 다행히 보완할 수 있는 방법 역시 있었고요. 저희는 UI테스트가 한번 실패한 경우, 간헐적인 테스트 실패 케이스를 고려하여 한 번 이상의 테스트를 다시 수행합니다. 1회 테스트의 신뢰도가 낮다면 테스트를 반복 수행하여 신뢰도를 높이고, 동일한 실패가 검출될 경우에만 직접 확인하기로 하였죠. 이때는 되도록 빠른 시일 내에 실패 케이스의 원인을 분석하여 수정해 주는 게 좋습니다. 테스트의 신뢰도가 낮고, 테스트 코드의 수명마저 짧다 보니 자칫 구성원들이 테스트 실패에 무뎌지는 상황이 자주 연출되었거든요. 이는 살충제 패러독스보다 더 나쁜 상황입니다.
이러한 이유로 저희는 몇 차례 시행착오 이후, UI테스트의 실행 트리거는 주요 브랜치(develop, release)에 보조 브랜치(feature, hotfix) 가 Merge 될 때로 정했습니다. 전통적인 CICD의 의미로는 테스트가 성공한 후에 코드를 병합하는 방향에 가까울 겁니다. 하지만.. UI테스트는 1회 테스트 수행에 소요되는 시간이 30분에 육박합니다. 그래서 코드 병합에 있어서도 유연하게 적용하기로 했습니다. 코드 병합을 먼저 하되, 그 실행 결과가 문제가 있는지 UI테스트가 검사를 진행하도록요. 새롭게 합쳐질 브랜치에 포함된 리팩토링의 영향도를 검사하는 UI통합 테스트의 개념도 함께 진행되는 것이죠.
아래에 UI테스트 자동화를 포함하는 전체적인 파이프라인을 도표로 정리해 보았습니다.
도표 내, CICD 단계에서는 Github 트리거 → TeamCity → CI Agent → Fastlane → Xcode UITest 순으로 구성됩니다.
사내에서 사용하는 CICD 툴은 TeamCity이고 Xcode 빌드 도구로 Fastlane 을 사용 중입니다. 다행스럽게 Fastlane에서 UITest를 위한 함수(lane)을 지원해 주므로 이 run_tests 함수(lane)를 호출함으로써 간단히 테스트를 시작할 수 있습니다.
Fastlane - ui_testing lane
또, 함수의 매개변수 구성에서 슬랙 노티피케이션 전송테스트 결과 리포트도 제공해 주는 걸 알 수 있습니다. 아래 슬랙 메시지 예시와 같이요!
Slack Notification
저희는 아래와 같이 UI테스트 결과를 유관부서로 전달함으로써, 새로운 피처 브랜치가 머지 된 후 기존 코드의 기본적인 동작에 문제가 없음을 검증하는 스모크 테스트 결과를 알릴 수 있습니다.
UI테스트 실패 시는, 위와 같이 보여주기도 한답니다.
테스트 리포트를 메시지와 함께 공유한다면, UI테스트의 직접적인 담당자가 아니라도 테스트 검증 결과를 좀 더 쉽게 알아볼 수도 있습니다.

마치며

지금까지 닥다 iOS 개발팀에서 UI테스트를 도입했던 경험들에 대해서 소개 드렸습니다.
이 아티클을 작성하면서 다양한 개발팀에서의 경험담과 후기글들을 참고했습니다. 각 글들 마다도 다양한 방식으로 UI테스트를 도입하고, 이루고자 하는 목표도 역시 다르다는 느낌을 받았습니다. 정말 괴물처럼 고수인 분들이 많기도 했더라는..
특히 QA 부서와 함께 테스트를 관리하는 곳이 인상 깊었습니다.
이 글을 쓰며 리서치를 많이 했는데, UI테스트와 그 자동화를 안정적이며 체계적으로 진행할 수 있게 도와주는 몇몇 기술들을 알게 되었고 그들을 도입 당시에 적용하지 못했다는 아쉬움이 남습니다.
저도 이 프로젝트 이후에 UI테스트를 다시 도입할 때가 되면 조금 더 체계적인 테스트 코드, 테스트 케이스, 자동화 구조에 대한 학습의 필요성을 느꼈습니다.
만약 이 글을 읽으신 독자분들 중에서 UI테스트 자동화 도입에 대해 고민하고 있다면! 꼭 기대하는 목표가 무엇인지 충분히 고민 후 도입하시는 걸 추천합니다. 나아가 많은 기술 팁과 도구들이 있으니 충분한 리서치와 고민을 하신 후 적용하시는 것을 추천드립니다.
긴 글 읽어주셔서 감사합니다

참고 자료