사실 Best Practice라는게 존재하기는 어려울 것 같다. 왜냐하면 테스트해야하는 프로덕트가 천차만별이고, 해당 프로덕트의 도메인도 가지각색이기 때문이다.
그래서 딱 이게 정답이야!! 이런 건 없고, 다만 테스트 대상, 팀의 리소스 현황, 기술 숙련도 등을 고려해서 최대한 우리팀에 최적화된 어떠한 방법들을 취사선택해서 진행해야한다.
아래는 내가 업무하면서 겪었던 문제들을 조금 리스트업해본 것이고.. 정답은 없는 것 같다.
적당히 머리에서 생각나는대로 그냥 끄적이고 있으므로 잘 정리되진 않은 글이기도 하니, 이 글을 읽는 분들께는 미리 양해의 말씀을 드리고 싶다..
테스트 자동화의 주요 목적에는, 기존 테스트인력의 리소스 절감 및 테스트 수행 속도 단축을 위한 것이라고 생각한다. 이 목적을 검증하기 위해서는 대략적으로 아래의 것들이 필요하다.
나는 여기서 커버리지에 대해 생각해봐야할 점이 있다고 생각한다. (2번의 경우는 꽤나 단순하게 도출해낼 수 있다)
테스트 자동화에서 커버리지
라고 한다면 보통은 기존에 수동으로 수행하던 회귀테스트케이스 셋 대비 얼마만큼 자동화되었는지를 떠올리는 사람들이 많을 것이다. 물론 이것도 방법이긴한데, 다른 방법으로는 요구사항에 대한 커버리지를 생각해 볼수도 있다. 즉, 요구사항에 기재된 기능 대비 얼마만큼의 테스트를 수행하고 있는지를 계산하는 것이다.
근데 그럼 회귀테스트케이스가 이미 요구사항 대비 커버리지를 산출해낼 데이터를 가지고 있을텐데 왜 따로 계산하냐는 의문이 들 수 있다.
자동화를 수행할 회귀테스트케이스 셋이 과연 자동화하기 적절한가?? 라는 것을 생각해볼 필요가 있다.
적절하지 않은 경우는 아래가 있을 것이다.
이러한 경우에 테스트케이스를 1:1 관계로 자동화하기가 어려울 수 있다. 아무래도 사람이 직접 수동으로, 눈으로 확인해가면서 수행하는 테스트이기 때문에, 테스트 케이스가 위와 같이 작성될 수도 있다고는 생각한다. 그러나 이것을 그대로 자동화하기에는 적절하진 않다.
위 예시에서 1번의 경우에는 2번 3번의 경우 실행할 테스트 스크립트들에 의존관계를 명시해준다거나, 혹은 setup 플로우를 통해 필요한 데이터를 먼저 생성해놓고 중첩되는 동작을 수행해가며 각 테스트간의 의존관계를 줄인 상태로 확인해볼 수 있는데, 이 의존관계를 만들어줘야하는 단계가 실패하게 된다면, 그 단계와 의존관계가 있는 케이스들은 전부 실패하게 된다. 의존하는 스크립트 혹은 테스트 스텝이 수정된다면 의존관계에 있는 스크립트들을 전부 수정해야할 수도 있다.
2번의 경우에는 각각 다른 케이스로 쪼개서 확인해볼 수 있다. 어쨌거나 완벽한 1:1은 어렵고 해당 테스트 수행을 위해 뭔가 특별한 스크립트만의 단계가 실행되어야한다던지, 1:N
의 관계로 쪼개져서 N개의 자동화 스크립트를 다 구현해야지 기존 테스트케이스 1개를 커버한다 라는 개념으로 볼 수 있을 것이다.
이러한 방법은 자동화 작업 시에 들어가는 리소스 대비 적은 커버리지로 계산될 뿐만 아니라 추후 모수가 되는 테스트케이스의 수정사항 발생 시에 N개를 하나하나 확인해서 수정해야하는 번거로움이 생긴다.
첫 번째로는 기존 테스트케이스를 자동화에 적합한 형태로 수정 / 재작성하는 것이다. 가장 좋은 테스트케이스는 누가 와도 그대로 따라서 수행하기만 하면 명시되어져있는 기대결과를 확인할 수 있어야하며, 각각의 테스트케이스는 의존관계없이 개별적으로 수행할 수 있어야 좋은 테스트케이스라고 생각한다. 이렇게 작성한다면 사람이 수동으로 실행하는 것은 물론이고, 사람이 해당 내용을 바탕으로 자동화하기에도 부족함없는 좋은 케이스가 될 것 이다.
때문에 기존의 잘 작성되어있지 않은 테스트케이스를 그대로 자동화하는 것이 아니라, 해당 케이스의 내용을 바탕으로 자동화하기에 적절한 시나리오/케이스로 재작성하여 자동화를 수행하는 것이다.
두 번째로는 새로운 품질을 정의하는 것이다. 즉, 테스트케이스를 자동화에 적합하게 다시 작성한다던지, 자동 테스트용 테스트케이스를 별도로 새롭게 작성한다던지이다. 이 경우에 그럼 기준을 어떻게 잡을 것이냐의 대한 논의가 진행되어야하며, 커버리지의 기준 또한 어떻게 가져가야할지 고민해봐야한다.
세 번째로는 커버리지 대상을 기존의 회귀 테스트케이스셋이 아니라 요구사항에 대해 직접적인 커버리지를 산출하는 방법이다. 이 방법은 요구사항 문서가 제대로 잘 갖추어져있으면서 각각의 요구사항에 대해 넘버링이 가능할 수준으로 확실하게 구분이 가능해야한다는 전제조건이 따른다.
이 세 방법 무엇을 하던 간에 리소스가 적지않게 들어가는 것이 사실이다. 따라서 현재 테스트케이스가 자동화하기에는 적합하지 않으니까 무턱대고 갈아엎을 수도 없다.
자동화를 할 대상 테스트의 형태로는
가 있을 수 있다.
아마 E2E테스트, 인수 테스트의 형태라면 시나리오의 구조로 작성되어져있는 곳이 많을 것이고, 그렇지않고 각각의 기능 테스트를 할 때 사용되었던 테스트케이스를 회귀 테스트케이스 셋으로 다시 이용하는 곳은 시나리오 형태가 아닌 케이스 형태로 된 곳이 많을 것이다.
테스트 자동화 또한 시나리오의 형태로 작성할 지, 케이스의 형태로 작성할지를 선택해야한다.
시나리오의 형태로 작성한다면, 각각의 기능 케이스보다 확인하는 assertion point가 적을 수 있어서 놓치는 이슈가 있을 수도 있다. 또한 시나리오 진행 도중에 에러가 발생한다면 시나리오 전체를 완수할 수 없으므로 다음 시나리오를 프로덕트의 초기 상태부터 실행하는 로직을 고려해야한다.
테스트케이스 형태로 작성한다면 각각의 테스트케이스는 다른 케이스와의 의존관계를 낮추어 하나의 케이스가 독립적으로 실행할 수 있도록 형태를 고민해야한다. 또한 작은 부분부분으로 확인하는 만큼 실행하는 테스트케이스의 갯수가 많아져, 전체 실행속도를 고민해야할 수도 있다.
어떠한 형태가 프로덕트와 팀 수행에 적합한지를 고민해야한다.
테스트 자동화 스크립트를 작성하면서 많이 고민했던 것은 유지보수하고 이해하기 쉬운 코드 작성이었다. 코드 자체를 작성하는 것은 사실 Clean code 를 비롯하여, 많은 개발서적이 있어서 참고하면 되긴한다. 나도 책을 전부 다 읽은 것은 아니고 부분부분 참고해보긴 했다.
비즈니스 용어라던지 자주 접하는 단어들에 대해서는 줄여서 쓰는 경우가 있다. 근데 이게 정말 줄여서 쓰는 것이 좋을까? 라는 고민은 한 번 즈음 가지고 사용하면 좋을 것 같다. 유지보수는 특히 신규 인력이 충원되는 경우, 혹은 다른 기능 개발 담당자가 담당 기능이 바뀌는 경우 기존 코드를 이어받아 작업을 해야할 때 문제가 발생하게 되는데, 이 경우에서 축약어에 대해서 이해하지 못한다거나, 이미 퇴사한 사람에게 연락해서 이 코드는 뭐에요?? 라며 물어보는 일이 발생할 수도 있다.
따라서 축약어를 사용할 때는 정말 이 단어를 줄이는 것이 맞는지 다시 한 번 생각해보면 좋을 것 같고, 어떤 축약어를 사용하기로 결정했다면, 그것들에 대해 원 단어는 무엇인지 문서로 한 번 정리를 하는 것이 좋다.
축약어는 문제는 Button, Label, Toggle등과 같은 단순 UI 컴포넌트들을 넘어서 비즈니스 용어나 프로덕트 내 고유 용어에도 해당되는 사항이다. 무엇을 어떠한 단어로 어떻게 줄일지는 최대한 고민하고, 팀원과 협의하에 정의하는 것이 좋다.
적절한 인터페이스와 추상화는 스크립트의 중복 작성을 줄여줄 뿐만 아니라 유지보수가 쉬운 아키텍쳐를 만들도록 도와준다. 테스트 자동화 스크립트도 하나의 소프트웨어라고 생각한다면, 유지보수가 쉬운 코드 컨벤션은 물론이고 그 구조 또한 따지지 않을 수 없다.
요새 많은 기업에서 기존 테스트의 자동화를 수행하고 있고, 발표되는 각종 기술 블로그들을 참고해보면 이제는 Page Object Model은 어느정도 다들 사용하는 것 같다. 어떠한 단순한 행위는 BasePage
로 대표되는 부모 클래스에 정의를 한 다음, 그것을 상속받아 사용하는 그러한 형태는 기술 블로그를 포함하여 많은 곳에서 레퍼런스들을 찾아볼 수 있다.
그것에서 더 나아가 테스트를 실행할 어떠한 플랫폼(Android/iOS/Web 등)에 종속되지 않는 테스트 코드를 작성하는 것에 대해서 인터페이스를 통한 추상화가 가능하다. 인터페이스를 사용한다면 해당 인터페이스를 구현하는 객체에 대해 동작을 강제할 수 있다.
문제는 이러한 것을 하기 위해서는
이러한 조건이 필요하다고 생각한다.
일단 예시를 예제 코드로 나타내면 아래와 같을 것 같다.
from abc import ABC, abstractmethod
class HomePageInterface(ABC):
@abstractmethod
def search(self, query):
pass
@abstractmethod
def is_home_page_logo_visible(self):
pass
class SearchResultPageInterface(ABC):
@abstractmethod
def get_first_item_in_search_result_area(self):
pass
@abstractmethod
def is_search_result_page_logo_visible(self):
pass
from HomePageInterface import HomePageInterface
from SearchResultPageInterface import SearchResultPageInterface
class AndroidHomePage(HomePageInterface):
def __init__(self, driver):
self.driver = driver
def search(self, query):
# Implementation for Android
pass
def is_home_page_logo_visible(self):
# Implementation for Android
return True
class iOSHomePage(HomePageInterface):
def __init__(self, driver):
self.driver = driver
def search(self, query):
# Implementation for iOS
pass
def is_home_page_logo_visible(self):
# Implementation for iOS
return True
class AndroidSearchResultPage(SearchResultPageInterface):
def __init__(self, driver):
self.driver = driver
def get_first_item_in_search_result_area(self):
# Implementation for Android
return "test"
def is_search_result_page_logo_visible(self):
# Implementation for Android
return True
class iOSSearchResultPage(SearchResultPageInterface):
def __init__(self, driver):
self.driver = driver
def get_first_item_in_search_result_area(self):
# Implementation for iOS
return "test"
def is_search_result_page_logo_visible(self):
# Implementation for iOS
return True
class TestSearch:
def test_001(self, page_factory):
home_page : HomePageInterface = page_factory.get_home_page()
assert home_page.is_home_page_logo_visible()
def test_002(self, page_factory):
home_page : HomePageInterface = page_factory.get_home_page()
home_page.search("test")
search_result_page : SearchResultPageInterface = page_factory.get_search_result_page()
assert search_result_page.is_search_result_page_logo_visible()
assert search_result_page.get_first_item_in_search_result_area() == "test"
이렇게 한다면 플랫폼에 종속적이지 않은 테스트 스크립트를 한 벌만 작성해서 관리할 수도 있을 것이다.
보기만 하면 테스트 스크립트에 대한 작성/유지보수 리소스가 개선되고 참 좋을 것 같은데, 문제는 이 추상화의 정도, 행위의 정의, 동작한 후에 어떤 element, 상태를 확인할 것인지를 미리 설계가 되어있어야한다.
단순하게 로그인하는 행위, 검색을 하는 행위정도는 login()
, search()
정도로도 정의할 수는 있지만, 많은 프로덕트들에서 어떠한 조작을 이렇게 단순한 행위로 정의할 수 있는 것은 별로 많지가 않다.
프로덕트의 이해도가 낮은 상태에서 이 작업을 수행하게 된다면 계속해서 필요한 추상 메소드들이 생겨날 수도 있고, 프로덕트의 UX가 플랫폼마다 다르다면 하나의 조작에 대해 통일화를 시키기가 어려운 경우가 있을 수 있다.
플랫폼별로 나누어서 테스트 스크립트를 작성하게 된다면 오히려 플랫폼별로 특이한 상황이나 행동에 대해 조금 더 유연하게 대처할 수도 있다.
따라서 프로덕트의 상태나 작업자의 프로덕트 동작에 대한 이해도, 조작플로우의 구현 난이도 등을 파악해서 인터페이스 사용여부나 추상화 정도를 판단한 후에 결정을 해야한다.
테스팅 프레임워크에서 제공하는 assert
키워드는 내가 기대한 결가와 실제 결과가 일치하냐 아니냐를 기계적으로 판단해주는 유용한 기능이다.
간혹 이 assert 키워드를 안쓰고 테스트 자동화를 했다고 하는 코드들을 본 경우가 있었는데, 이것은 잘못된 테스트 코드이다. assert가 없으면 이 테스트에 문제가 있는지 없는지 어떻게 판단을 할 것 인가? 그저 코드가 처음부터 끝가지 꺠지지만 않고 실행되면 된다는 식인가?
다만, 리포팅 툴에 따라서는 이러한 assert
없이, try ~ catch 로 테스트 스크립트를 감싸고, 어떠한 에러가 발생하면 해당 리포팅 툴로 연계해서 Pass/Fail 여부를 전송할 수도 있을 것 같다. 어쨌든 결과를 판단한다는 의미에서는 assert 키워드가 없다기보다는 assert라는 행위를 한다고 생각한다.
그러니까 즉, 리포팅 툴에 따라서 assert를 어떻게 할 것이냐 가 조금씩은 다를 수도 있을 것 같다.
테스트 시나리오의 경우 여러개의 assert
키워드가 존재할 수도 있는데, 이 때 일단은 테스트 스크립트가 끝까지 실행되길 원하면 soft assertion을 하면 좋고, 그렇지 않고 일단 이슈가 발생하는 건 하나하나 빠르게 피드백 받길 원한다면 hard assertion을 하면 좋을 것이다.
두 개 다 장단점이 있으니 경우에 따라서 적절히 이용하면 좋을 것 같다.
서비스중인 프로덕트는 게속해서 변경사항이 발생한다. 이 변경사항에는, 안건의 기획에 따라 아주 미세한 변경일 수도 있고, 아예 기존 기능을 없애버리고 새로운 기능 혹은 화면으로 대체하는 것일 수도 있다.
이러한 때에 UI 자동 테스트는 UI변경에 큰 영향을 받게된다. UX는 같거나 비슷하다고 해도 유니크한 identifier가 존재하지 않는다면, xpath의 경로가 많이 뒤바뀌는 경우가 있어서 어찌되었던 자동테스트 스크립트에서도 로케이터들을 수정해야하는 경우가 많이 발생한다.
UX가 같거나 비슷하면서, 화면의 요소가 달라지는 경우에 가장 유효한 방법은, 첫 번째로는 각각의 element에 대해 의미있는 id값을 부여해주고 한 화면 내에서 부여된 id에 대해 게속해서 관리를 해주는 방법이다.
이러한 ID값이 존재한다면, 화면 내 컴포넌트 배치라던지 요소가 바뀌어도 어떠한 특정한 목적의 의미를 가진 ID를 계속해서 사용해준다면, 자동테스트 스크립트를 크게 수정해야할 일은 없을 것이다.
두 번째로는, element가 Text값을 가졌다면, 그냥 그 Text값을 사용하는 것이다. 누군가는 text값을 locator로 사용하는 것에 대해 안좋은 의견을 가지고 있을 수 있는데, UX가 바뀌지 않는다거나 혹은 거의 비슷하다는 가정을 한다면, 기존에 화면 내에서 노출되던 텍스트도 거의 바뀌지 않을 가능성이 높다. (기존에 오랜 라이프사이클을 가진 프로덕트가 변경을 한다면 더더욱) 게다가 잘 계획된 기능/화면이라면, 어떠한 커머스 등의 프로덕트 도메인 고유의 특성으로 인한 경우를 제외한다면, 어떠한 텍스트에 대해서 중복적으로 사용되는 경우는 거의 없을 것 이다.
다국어 프로덕트에 대해 텍스트로 대응해야한다고 하면 그것은 조금 문제가 달라지긴 한다.
Xpath를 이용한 식별방식은 뭘 어떻게해도 UI변경에 매우 취약하다. 그러나 이것을 조금 덜 취약하게는 할 수 있는데 예를 들어서 고정된 특정 element를 기준으로 element를 찾아 내려가는 방식이다.
풀 스캔을 하는 방법으로는 스캔 시간이 많이 걸리긴하나, 특정 element를 바로 찾을 수 있다면 스캔시간이 또 그렇게 오래걸리진 않는다.
만약에 UI변경이 자주 일어난다면, UI컴포넌트를 변경하는 시점에 개발팀 측에 Identifier값을 심어달라고 하는 것이 제일 좋다. 물론 개발팀에서는 추가적인 리소스가 들어가니까 안좋아할 수 있지만, 시간이 걸려도 좋으니 심어달라고 요청하는 것이 좋다.
반대로 UI변경이 별로 발생하지 않는다면, 상대적 xpath를 통해 관리하는 방법이 좋을 수 있다.
모든 element에 identifier가 심어져있다면 그것이 정말 베스트이긴하지만, 그렇지 않은 경우가 대다수일 것이다. identifier가 있다면 자동화 작업을 할 때 편하긴 하지만, 그것을 심는 누군가의 리소스가 추가적으로 더 드는셈이니, 정말 이 작업을 의뢰할지말지, 의뢰하지않아도 조작이 가능한지 등의 여부를 조사하고 이러한 것들에 대해 검토해볼 필요가 있다.
끝! 2편은 언젠가 또 쓸 내용들이 떠오른다면...!