대상독자
- Template 기반 레거시 프로젝트에서도 테스트를 도입을 하고 싶은 분
- 시나리오 기반 인수테스트 작성 팁을 얻고 싶은 분
- Selenide 설정이 궁금하신 분
도입 배경
이전까진 RESTful API 기반 프로젝트 상에서 통합/단위 테스트를 작성했었습니다. 하지만 레거시 프로젝트를 이번에 맡게 되면서 테스트를 포기할까 생각을 했지만 코드를 안정적으로 리팩토링하려면 결국 통합테스트가 있을 수 밖에 없다는 결론이 났습니다. 그리고 프로젝트를 맡은지 얼마되지 않다보니 여기를 변경하면 다른 곳에 기능이 영향이 가는 경우가 종종 발생하여 QA 시간이 긴 문제도 있었습니다.
팀 내에서 QA 시간 단축 및 빠른 레거시 코드 개선을 효과로 내세워 설득하여 2주정도 작업을 해서 도입할 수 있었습니다.
인수테스트란
개발자가 개발한 것에 대해 다른 사람이 인수하기 위한 테스트입니다. 통합 테스트의 한 종류입니다. 예를 들어, API 를 개발했으면 이 API가 잘 작동하는지에 대해 검증하기 위한, 인수할 수 있는 정도인지 확인하는 테스트입니다. 이 글에서 다루는 인수 범위는 UI 까지 포함합니다. 그래서 End to End(E2E) Test 라고도 할 수 있습니다.
인수테스트가 필요한 이유는 개발자가 개발한 결과물이 요구사항에 부합하는지 인수자가 객관적으로 확인할 수 있기 때문입니다. 인수자도 이해할 수 있을 정도의 추상화로 시나리오를 작성해야한다는 특징이 있습니다. 당연히 인수자와 해당 시나리오를 공유해야 의미가 있습니다.
End To End(E2E) 테스트란
End to End 테스트로 웹앱 기준으로 사용자 브라우저부터 어플리케션을 거쳐 DB 단까지 테스트 범위에 넣는 테스트입니다. 사용자 레벨부터 테스트 범위가 있기 때문에 실제 서비스 구성 환경에 가깝게 통합 테스트 할 수 있다는 장점이 있습니다.
단점은 사용자 단의 브라우저, HTTP 서버, 웹앱서버, DB 서버까지 모든 시스템이 가동되어야 하는 테스트이기 때문에 테스트를 하나 가동시키는데 시간과 자원이 타테스트에 비해 비교적 많이 소비됩니다. 그래서 모든 케이스에 대해 또는 언제든 실행하기엔 부담이 큽니다. 범위가 작고 자원 소비가 적은 단위테스트와 함께 가야 효율적입니다.
주로 최소한의 기능 작동을 확보해야할 때 해당 기능에 최소흰 시나리오로 E2E 테스트를 적용합니다. 특정 Layer 변경에 따른 유지보수와 실행 비용이 크기 때문입니다.
테스트도구 선정
테스트를 유지보수하기가 좋으려면 적절한 도구를 사용해야 합니다. 적절한 도구를 사용하지 않으면 러닝커브가 높아지고 유지보수해야하는 코드가 증가하기 때문입니다.
- Cypress: 자바스크립트 기반 E2E 테스트 도구. GUI를 통해 테스트 시나리오를 작성할 수 있습니다.
- Selenide: 자바 API 기반 E2E 테스트 도구. 엔진은 파이썬 기반 브라우저 Automate(마치 매크로)인 Selenium 을 기반으로 합니다. Selenium 이라는 사용도와 인기도가 높은 도구를 코어로 하고 있기 때문에 신뢰가 높습니다.
도구를 선정할 때 기준으로는 Java API 를 지원하는지를 먼저 봤습니다. 왜냐하면, 단일 언어를 사용해야 테스트 코드 유지보수가 좋기 때문입니다. 다음으로 사람들이 많이 사용하고 있는지 유지보수가 최근까지 잘 되고 있는지 입니다. 사람들이 많이 사용할수록 버그도 적고 다양한 예외상황에 대해서 대응하기가 용이하기 때문입니다.
위 2가지 사항을 만족하는 Selenide 를 선택했습니다. Selenide는 Java API 기반이고, 엔진으로 Selenium 이라는 유명한 Browser Automate 이기 때문입니다.
테스트 설정
Gradle 환경 기준으로 Selenide Dependency를 추가합니다. version 은 최신버전 사용을 권장합니다. Quick Start,Selenide. 왜냐하면 기본으로 크롬 브라우저를 내부적으로 사용하며 테스트환경에 최신 크롬 브라우저가 설치되어 있고 Selenide 라이브러리 버전이 옛날 버전이면 크롬 실행이 제대로 안되는 문제가 발생합니다.
dependencies {
testImplementation 'com.codeborne:selenide:6.17.2'
testImplementation 'com.codeborne:selenide-core:6.17.2'
testImplementation 'org.seleniumhq.selenium:selenium-java:4.11.0'
testImplementation 'org.seleniumhq.selenium:selenium-api:4.11.0'
testImplementation 'org.seleniumhq.selenium:selenium-support:4.11.0'
testImplementation 'org.seleniumhq.selenium:selenium-chrome-driver:4.11.0'
testImplementation 'org.seleniumhq.selenium:selenium-remote-driver:4.11.0'
}
Selenide E2E 실행 설정은 com.codeborne.selenide
static 클래스 변수를 통해 합니다. static 클래스 설정이니 병렬로 테스트를 실행하기엔 다루기에 까다롭습니다. 하지만 일반적으로 순차적으로 테스트를 실행 시에는 괜찮습니다.
아래 설정 중에 headless 란 E2E 테스트와 브라우저에서 주로 사용하는 용어인데 headless 가 true 이면 사용자와 인터랙션할 수 있는 GUI 창을 띄우지 않습니다. 내부적으로만 띄우고 작동됩니다. 기본적으로 false 로 두고 테스트를 자동화합니다. 디버깅할 때 true 로 하여 사용합니다.
브라우저 실행은 아래 CommonE2eNavigator.class
에 있듯이 com.codeborne.selenide.Selenide.open()
static 메서드를 호출합니다.
public class E2eTest {
@BeforeAll
protected static void init(@LocalServerPort int port, @Value("${app.test.e2e.headless:true}") boolean isHeadless) throws Exception {
Configuration.baseUrl = String.format("http://localhost:%s", port);
if (isHeadless) {
Configuration.headless = true;
Configuration.holdBrowserOpen = false;
} else {
Configuration.headless = false;
Configuration.holdBrowserOpen = true;
}
}
}
인수테스트 시나리오대로 추상화하여 작성한 로그인 테스트 코드 예제입니다. 이렇게 개발자가 아닌 사람이 봐도 이해할 정도로 추상화가 필요한 이유는 인수테스트 존재 자체가 작업자가 아닌 사람에게 인수하기 위해 검증하는 테스트이기 때문입니다.
@DisplayName("[E2E] 로그인 관리")
class LoginE2eTest extends E2eTest {
@BeforeEach
void setup() {
홈_이동();
}
@DisplayName("로그인 성공")
@Test
void 로그인_성공() throws Exception {
// when
로그인_요청(SUPER_ADMIN_EMAIL, "1234");
// then
페이지_경로_확인("/");
알림_메시지_확인("로그인하였습니다.");
}
}
각 시나리오에 쓰이는 유저 액션에 대한 단계(Step)도 추상화하여 재사용성을 높였습닏다. 다른 시나리오에서도 쉽게 재사용할 수 있도록 했습니다.
기본적으로 '행위(스텝)'이 있고 하위에 '요청'과 '확인' 세부단계로 구성했습니다. 확인 단계를 둔 이유는 어떤 절차에서 실패했는지 빠르게 파악할 수 있기 때문입니다.
class UserE2eStep {
public static void 로그인(String id, String password) throws Exception {
로그인_요청(id, password);
로그인_결과_확인(id);
}
public static void 로그인_요청(String id, String password) throws Exception {
로그인_페이지_이동();
태그_선택("login-email-input").setValue(id);
태그_선택("login-password-input").setValue(password);
태그_선택("login-btn").click();
}
private static void 로그인_결과_확인(String id) throws Exception {
마이페이지_이동();
태그_선택("my-page-email").shouldBe(Condition.text(id));
}
}
Navigator 는 Step 클래스와 연관이 있으며 Element를 선택하거나 페이지를 이동하는 역할을 담당합니다.
Navigator를 또 Step 클래스에서 클래스를 따로 분리한 이유로는 Step 클래스에 요청과 확인 시 단순 페이지 이동에 관련된 코드들이 꽤 많이 들어갔고 다른 Step 클래스에서도 재사용되는 경우가 많아보였기 때문입니다.
public class UserE2eNavigator {
public static void 로그인_페이지_이동() throws Exception {
태그_선택("usermenu-login-page-link").click();
페이지_경로_확인("/login");
}
}
public class CommonE2eNavigator {
public static final String 타겟_속성_키 = "data-e2e-target";
public static final String 타겟_식별자_속성_키 = "data-e2e-target-id";
public static void 홈_이동() {
페이지_이동("/");
}
public static void 페이지_이동(String path) {
open(path);
}
public static SelenideElement 태그_선택(String attributeValue) {
return $(Selectors.byAttribute(타겟_속성_키, attributeValue));
}
public static void 종료() {
WebDriverRunner.getWebDriver().close();
}
}
팁들
com.codeborne.selenide.executeJavaScript
로 자바스크립트를 실행시킬 수 있다. 에디터 입력이나 JS 제어가 필요할 때 할 수 있다.
public static void 에디터_내용_입력(String inputId, String content) {
executeJavaScript(
String.format(
"document.getElementById('%s_ifr')"
+ ".contentWindow.document.querySelector('body[data-id=%s]')"
+ ".innerHTML='<p>%s</p>'", inputId, inputId, content));
}
Alert 창 처리는 다음과 같은 API로 처리 할 수 있다.
Selenide.switchTo().alert().accept();
트러블슈팅: 데이터 변경이 필요한 경우
이메일 인증 발송,확인,휴면처리와 같은 절차는 UI로만 하지 못하기 때문에 Repository 를 통해 데이터를 변경하는 과정이 필요한 경우도 있다.
이렇게하면 클래스 레벨로 트랜잭션 범위로 설정하면 변경사항이 commit 되지 않고 E2E 테스트가 진행되서 테스트가 실패한다.
아래 예제에서 각 변경사항 별로 트랜잭션을 만들고 바로 commit 되도록 했다.
public void 트랜잭션(Runnable callback) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) {
callback.run();
}
});
}
public void 마지막_로그인일자_수정(String email, LocalDateTime time) {
트랜잭션(() -> {
User 유저 = userSearchService.findByEmail(email);
유저.setLoginDate(time);
userService.save(유저);
});
}
후기
- E2E 테스트이자 인수테스트를 도입해서 6개월 이상 사용해보고 있다. 확실히 전 테스트가 돌릴 때 시간이 많이 소요됨을 절실히 느끼고 있습니다. 배포 전 단계에서만 적용하다면 좋을 것 같습니다.
- 병렬 실행으로 개선해보려고 했지만 로컬에선 잘 작동을 하지만, 스레드수가 적은 CICD 환경 상에서 리소스에 비해 너무 많은 스레드가 생성되어 각 테스트에서 타임아웃이 발생하여 테스트가 실패하는 난관에 부딪쳤습니다. 현재 여러 실험 중에 있습니다.
- 성과로는 QA 시간을 단축시킬 수 있었고, 테스트가 존재하는 기능들에는 리팩토링을 과감하게 해볼 수 있었습니다.
- 템플릿 기반 레거시 프로젝트에서도 이렇게 Selenide 도구를 통해 테스트를 구축해볼 수 있습니다.
'공부노트' 카테고리의 다른 글
웹브라우저 JS에서 비동기처리하는 방법 (0) | 2023.04.20 |
---|---|
ERD 란 무엇이고 어떻게 사용할까? (0) | 2021.04.11 |
Ruby 언어 배우기 (0) | 2021.03.07 |
dpkg 패키지 매니저에서 깨진 패키지 강제 삭제하기 (0) | 2021.02.28 |
ruby server 실행 시, bundler 실행 버그 (0) | 2021.02.24 |