brunch

You can make anything
by writing

C.S.Lewis

by 이지원 Nov 06. 2022

Stream API를 활용한 Selenium 자동화

Java_E2E Test Automation

E2E 테스트 자동화는 데이터를 저장하고 접근하여 처리하고 반환하고 출력하는 로직을 구현해야 하기 때문에 팀에서 활용 중인 언어의 내부 특성을 이해하고 원활한 협업을 위해 보다 간결한 코드를 작성하는 역량이 필요합니다.


Webdriver에서 제공하는 메서드만으로는 정말 간단한 테스트 시나리오만 구현 가능하기 때문에 테스트 자동화 커버리지를 높이는 것에 한계가 있습니다. 해당 맥락에서의 한계는, '자동화가 불가능하고 안 되는 것이 아니라 로직을 구현해내지 못함'에서 발생하는 한계를 의미합니다. 


클릭하고 입력하고 검증하는 메서드는 이미 Webdriver에서 제공하기 때문에 클릭<->입력 <->검증의 테스트 흐름이 복잡하지 않을 경우 쉽게 자동화 가능하지만 실무에서 주어지는 요구사항을 분석하다 보면 특정한 데이터를 가공하여 테스트 로직에 활용해야 하는 경우가 있습니다. 이러한 상황에서는 프론트나 백엔드 프레임워크를 활용하여 Feature를 구현하는 것처럼 E2E 테스트 자동화에서도 원하는 테스트 결과 검증을 위한 로직 구현이 필요합니다.


매뉴얼 테스트 프로세스와 테스트 설계가 잘 진행되는 조직일수록 테스트 자동화 시나리오의 난이도는 증가합니다. 테스트 자동화 시나리오의 난이도가 높다는 의미는 무엇을 자동화해야 할지 명확한 상태임을 의미합니다. 테스트 자동화 난이도가 낮은 조직은 무엇을 자동화시켜야 할 지에 대한 테스트 고도화 작업이 진행되기 전이므로 대부분 Webdriver에서 제공하는 메서드를 활용하여 클릭<->입력<->검증의 순차적인 패턴으로 쉽게 자동화 가능합니다. 


자동화하기 전 먼저 고려해야 할 사항은 '매뉴얼 테스트 프로세스와 산출물이 얼마나 잘 관리되고 있는지?'입니다. 개인적으로 매뉴얼 테스트 체계가 잡혀있지 않은 조직에서의 자동화는 크게 의미가 없다고 생각합니다. 매뉴얼 프로세스가 잡히지 않는 상황에서의 자동화는 추후 리팩토링 비용만 증가시키기 때문입니다. 


이번 포스팅에서는 Java Stream API를 활용하여 코드를 보다 직관적이고 간결하게 리팩토링 하는 방법을 작성해보았습니다. ES6의 Template Literal, Desturcturing, map, forEach, filter, reduce, Lambda, 삼항 연산자를 활용할 경우 JavaScript 테스트 코드를 보다 편리하고 간결하게 구현할 수 있는 것처럼, 완전히 동일하진 않겠지만 비슷한 맥락으로 Java에서는 Java SE 8부터 추가된 Stream API를 활용한다면 보다 간결한 코드를 구현할 수 있습니다.


Java Stream API?

Java에서는 데이터를 저장하기 위해 배열 또는 컬렉션을 사용합니다. 간단한 시나리오가 아닌 이상 데이터를 담아야 하는 공간이 필요하고 저장된 데이터에 접근하기 위해 반복문이 필요합니다. 시나리오 복잡도가 증가할수록 코드가 길어지고 가독성도 떨어집니다. 


만약 2명 이상의 테스트 자동화 엔지니어가 코드 리뷰 후 머지를 원칙으로 하는 협업 상황에서 Stream API를 활용하지 않을 경우 반복문과 조건문 안에서 처리되는 로직에 대한 주석과 더불어 전체적인 코드 흐름 파악에 보다 많은 시간이 소요될 수 있습니다. 특히 Python에 비해 복잡한 구문을 지닌 Java에서는 더욱 그렇습니다. 


위와 같은 문제점을 극복하기 위해 Java SE 8에서 Stream API가 도입되었습니다. Stream API는 데이터를 읽고 쓰기 위한 공통된 방법을 제공하기 때문에 직관적이고 간결한 코드를 작성할 수 있습니다. Stream API의 특징으로는 원본 데이터를 변경하지 않고 filter-map 기반의 API를 사용하여 지연(lazy) 연산을 통해 성능 최적화가 가능합니다.


알아볼 내용

보다 쉬운 예시를 제공하고자 몇 가지 상황을 가정합니다. 이번 블로깅에 등장하는 사이트는 예시를 구성하고자 살펴본 사이트입니다.


테스트 요구사항

개요 및 매뉴얼 테스트 관점

원티드 메인 페이지 상단에는 채용, 이벤트, 직군별 연봉, 이력서, 커뮤니티, 프리랜서, AI 합격예측 메뉴가 존재한다. 각 메뉴의 텍스트 값이 Production 환경에서 정상적으로 나타나고 있는지 검증에 필요한 공통 메서드 구현이 필요하다. 검증에 필요한 공통 메서드를 구현하기 전 우선 각 메뉴의 텍스트 값을 String Type으로 출력하는 메서드를 구현해보자.


자동화 테스트 관점

1. 메뉴 텍스트 값이 변경되어도 코드 수정 없이 변경된 값과 검증 가능해야 한다.

2. 빈 문자열이 아닌 경우만 데이터를 추가해야 한다.

3. 중복을 제거해야 한다.


예를 들어 위와 같은 요구사항이 실무에서 주어졌다면 어떻게 해야 할까요? 여러 방법이 있겠지만 우선 실무에서 가장 간단하지만 피해야 할 구현 방법에 대해 살펴보겠습니다. 


피해야 할 구현 방법

정확한 명칭이 존재하는지는 모르겠지만, 이러한 방식으로 구현된 자동화를 선형 구조 패턴이라고 하겠습니다. 선형 구조 패턴을 사용하면 자동화 테스트 관점과는 무관하게 일단 동작하는 코드 구현은 가능합니다. 하지만 구현 가능성을 검토하는 PoC 기간이 아닐 경우엔 가급적이면 사용하지 않을 것을 권장합니다. 


1. 각 메뉴의 고유한 로케이터를 String 변수에 하나씩 할당합니다. 메뉴 1개당 변수 1개가 할당되므로 N개의 String 변수가 작성됩니다. 


2. getText()로 텍스트를 가져와서 출력합니다.


3. 이후 Assert를 통해 검증합니다.


만약 위와 같은 방식으로 테스트 코드를 구현할 경우 테스트 시나리오를 구현하는 순간부터 유지보수에 어려움을 겪게 됩니다. 관리해야 할 테스트 로직뿐 아니라 한 개의 메서드로 충분히 재사용성 있게 처리 가능한 로직을 여러 개의 메서드로 해결해야 하는 상황이 생깁니다. 가장 큰 문제는 상단 메뉴의 텍스트 값이 변경될 경우 코드 수정 범위가 넓어지기 때문에 사실상 실무에서는 사용하기 힘든 방법입니다. 


위처럼 진행되는 자동화는 결국 테스트는 100% 사람이 해야 한다는 결론에 도달하기 가장 빠르고 쉬우며 코드 레벨 자동화뿐 아니라 자동화 자체에 대한 불신을 가지게 됩니다. E2E 테스트 자동화 코드는 로케이터 변경과 실제 서비스 장애가 아니라면 코드 수정 범위를 최소화시키고 재사용 가능한 방향으로 구현 필요합니다. 


테스트 요구사항에서 매뉴얼 테스트 관점과 자동화 테스트 관점에 적합한 형태로 구현하기 위해 Java Collection의 집합(Set) 자료구조를 활용하여 구현해보겠습니다.


권장 구현 방법

예시 블로깅이다 보니 main 메서드가 존재하는 파일에서 모든 로직을 처리했습니다. 실무에서는 페이지 개체로 설계하여 유지 보수하는 것을 권장합니다.

elements 리스트에 메뉴에 해당하는 ul 태그의 자식 요소를 로케이터로 생성했습니다. 위 요구사항에서 로케이터 생성의 핵심은 리스트에 담을 수 있는 고유한 값으로만 생성하면 됩니다. 어차피 고유한 값으로 생성하려다 보면 성능이 나쁜 로케이터 생성은 하지 않게 됩니다. id(1순위)가 아니라면 상황에 맞게 cssSelector 또는 xpath를 고유한 값으로 만들어서 사용하는 것이 좋습니다.

https://gist.github.com/Jiveloper/f052635c6813c0741185b8ccecf4065d

정적 메서드 beforeImprovement를 만들어 자동화 테스트 관점의 요구사항을 만족하도록 구현된 모습입니다. 


beforeImprovement에 로케이터를 받아서 반복문을 통해 텍스트를 가져오고 빈 문자열이 아닌 경우에만 데이터를 추가하여 category를 출력합니다. 


중복된 텍스트를 제거하기 위해 동일한 원소를 저장하더라도 저장소에 중복된 값이 존재하지 않도록 하는 자료구조 HashSet을 활용했습니다. 메뉴 같은 경우는 데이터의 크기가 어느 정도 예상되고 길이를 체크한 다음 데이터를 삽입할 것이기 때문에 HashSet을 활용했습니다. 


만약 검증하려는 메뉴가 알파벳이나 숫자이고 정렬된 형태로 값을 확인하고 싶다면 HashMap으로 만들어진 HashSet보단, 정렬되어 출력하는 TreeSet과 같은 자료구조를 활용하는 것이 좋을 것 같습니다.

https://gist.github.com/Jiveloper/6dc82693f382f651838cca619e64468c

해당 로직을 실행하면 위와 같은 결과가 출력됩니다. 검증에 필요한 공통 메서드를 구현하기 전 우선 각 메뉴의 텍스트 값을 String Type으로 출력하는 메서드를 구현했고, 자동화 테스트 관점에서 3가지 요구사항도 만족했습니다. 


1. 메뉴 텍스트 값이 변경되어도 코드 수정 없이 변경된 값과 검증 가능하다.

elements List에 메뉴에 해당하는 ul 태그의 자식 요소를 로케이터로 생성하여 List에 담은 뒤 활용했기 때문에 텍스트 값이 변경되어도 수정 범위가 없습니다. 또한 메뉴가 추가되어도 ul 태그의 자식 요소를 로케이터로 활용하면서 반복문을 실행하기 때문에 1번 요구사항을 만족했습니다.


2. 빈 문자열이 아닌 경우만 데이터를 추가해야 한다.

beforeImprovement 정적 메서드에서 전달받은 엘리먼트를 순회하면서 엘리먼트 텍스트가 담긴 category의 길이가 0보다 클 경우에만 출력하는 로직이 구현되었기에 2번 요구사항도 만족했습니다.


3. 중복을 제거해야 한다.

HashSet을 활용하여 데이터를 삽입했기 때문에 3번 요구사항도 만족했습니다.


결과적으로 피해야 할 구현 방법에 비해서 조금 더 프로그래밍적인 사고와 관점으로 실무에서 주어지는 요구사항을 해결했습니다. 이러한 방법으로 테스트 로직을 구현해도 동작에는 문제가 없지만 시나리오에 따라 코드 길이가 길어지고 가독성도 떨어질뿐더러 엔지니어마다 다른 방식으로 구현하게 될 가능성이 있습니다. 


이러한 문제점을 극복하기 위해서 Java SE 8부터 스트림(stream) API를 도입하게 되었습니다. 동일한 구현 사항을 스트림 API로 리팩토링 하겠습니다.


Stream API를 활용한 리팩토링

https://gist.github.com/Jiveloper/f10da97441668674e46a54de5b917eac

우선 리스트에 담긴 로케이터를 스트림으로 변환합니다. 로케이터의 값을 가져오기 위해서 map을 사용합니다. 중복을 제거하기 위해서 distinct를 사용합니다. 길이가 0보다 큰 경우에만 값을 찾도록 filter를 사용합니다. 마지막으로 위 모든 작업을 수행 후 출력하기 위해 forEach를 사용합니다.

정말 간단하지 않나요? 스트림으로 변환 후 마치 물 흐르듯이 코드를 작성하였고 앞서 리팩터링 전 코드와 동일한 결과가 출력되는 모습입니다.

https://gist.github.com/Jiveloper/f88fded402a661c1e255cffe303c46d6

동일한 기능을 수행하지만 stream을 활용할 경우보다 간결하고 직관적인 테스트 코드 구현이 가능합니다. 지금은 간단한 테스트 요구사항이기 때문에 큰 차이를 못 느낄 수도 있습니다. 


하지만 더욱 복잡하고 다양한 요구사항에 대응하기 위한 코드를 구현하다 보면 메서드 안의 for문과 조건 처리에 필요한 로직이 많아집니다. 협업의 관점에서도 stream으로 구현된 테스트 코드 같은 경우 map과 distinct와 filter와 forEach가 내부적으로 어떻게 구현되었는지 모르더라도 사용 방법만 익힌다면 어떠한 기능을 하는 메서드인지 쉽게 파악 가능한 장점이 있습니다. 


이상으로 Stream API를 활용한 Selenium 자동화 블로깅을 마치겠습니다. 테스트 코드 구현과 프레임워크 설계에 있어서 보다 깔끔하고 간결한 로직을 구현할 수 있도록 사용하는 언어의 특성과 메모리 관점에서의 동작을 이해하고, 보다 간결한 코드 작성을 위해 도입된 기능에 대해 학습하여 생산성 높은 코드를 작성할 수 있는 QA Engineer가 되도록 노력해야겠습니다. 


감사합니다.

매거진의 이전글 Selenium 4.6.0 Released!
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari