이번 포스팅에서는 기능 자동화를 구성하기 위한 POM 구조에 대한 내용을 포스팅합니다.
자동화 테스트에서 왜 중요한지, 기본 개념과, 어떻게 적용할 수 있는지에 대해 포스팅하려고 합니다 😎
POM(Page object Model) 구조란 ?
POM 구조는 자동화 테스트를 구성할 때 가장 기본이 되는 설계 패턴입니다. 왜 가장 기본이 될까요 ?
페이지 단위로 모델링을 해야하는 장단점을 설명하는 것 보단 예시를 들어서 왜 필요한지 설명해보겠습니다 😀
오늘의 테스트 페이지는 네이버 날씨 페이지로 작성해보겠습니다 :)
우선 날씨 홈 화면 입니다. 상단에 날씨 관련 메뉴로 이동할 수 있는 "홈", "예보비교", "미세먼지", "지도", "영상", "기상특보" 페이지가 보이네요.
그리고 날씨 홈 화면에서 날씨 정보들을 확인할 수 있다는 것을 알 수 있습니다.
테스트를 하기 위한 간단한 테스트 시나리오는 아래와 같습니다.
1. 네이버 날씨 페이지로 이동
2. 상단 메뉴 항목을 한번씩 클릭
3. "홈" 화면으로 이동하여 "전국날씨"에 요일 별 탭 버튼 한번씩 클릭하기
먼저 테스트 수행한 결과 영상을 공유드립니다.
간단한 페이지 이동 및 특정 탭 버튼 클릭 자동화 테스트입니다.
이 테스트를 페이지 구조를 만들지 않고 동작한다면 어떤 코드가 생성될까요?
const weeklyList ='ul.weekly_list[role=tablist]';
const contentLayer = 'div#content';
test.beforeEach(async ({page}) => {
mainPage = new MainPage(page);
});
test('네이버 날씨 테스트 중', async ({ page }) => {
const comparePageButton = 'div.menu_area >> a >> text="예보비교"';
const mainPageButton = 'div.menu_area >> a >> text="홈"';
const airPageButton = 'div.menu_area >> a >> text="미세먼지"';
const mapPageButton = 'div.menu_area >> a >> text="지도"';
const warningPageButton = 'div.menu_area >> a >> text="기상특보"';
await mainPage.gotoPage();
await page.locator(comparePageButton).click();
await page.waitForTimeout(2000);
await page.locator(airPageButton).click();
await page.waitForTimeout(2000);
await page.locator(mapPageButton).click();
await page.waitForTimeout(2000);
await page.locator(warningPageButton).click();
await page.waitForTimeout(2000);
await page.locator(mainPageButton).click();
await page.waitForTimeout(2000);
//...전국날씨 탭 확인 기능 ...
});
테스트 코드, 혹은 하나의 클래스 파일에 여러 페이지에서 사용할 식별자들이 들어갈 수 있고, 서로 다른 페이지에서 사용하는 식별자들이 겹치기 때문에 기능이 정상 동작하지 않을 수 있습니다.
애초에 개발 할 때도 코드 분리를 하기 때문에 기능 테스트 자동화를 구성할 때에도 모델링이 필수적으로 필요합니다.
왜 페이지 모델을 만들어야하는지 설명이 되었을까요..?
그럼 어떤 기준으로 만드는 것이 좋을까요 ?
제가 생각하는 방식은 이렇습니다.
1. 모든 페이지에 항상 보여지는 메뉴가 있다면 별도 클래스로 추출
2. 페이지 별 클래스 생성
3. 페이지 내 기능이 많은 경우 서브 페이지 클래스 생성
4. 공통 기능 클래스 생성
이런 기준으로 클래스를 생성하며 모델링을 진행한 구조는 아래와 같습니다.
naverPage/
│
├── NaverWeatherCompare.js
├── NaverWeatherGnb.js
├── NaverWeatherMain.js
├── ...
│
tests/
│
└── naverTests/
└── NaverWeatherMain.test.js
항상 표시되는 글로벌 메뉴는 Gnb 클래스로 별도 추출하였고, 각 페이지 별 클래스를 생성하였습니다.
저는 단순히 클릭 동작만 하는 테스트를 만들었기에, 공통 클래스는 없지만 중복되는 코드가 많은 경우 Common으로 생성하여 관리합니다.
그럼 각각의 클래스에는 어떤 기능들이 들어가 있는지 확인해보겠습니다.
// NaverWeatherMain.js
const fs = require('fs');
const yaml = require('yaml');
// YAML 파일 읽고 파싱
const file = fs.readFileSync('./dataSet.yaml', 'utf8');
const yamlData = yaml.parse(file);
const nationLayer = 'div#nation';
const weeklyList ='ul.weekly_list[role=tablist]';
const contentLayer = 'div#content';
class NaverWeatherMain {
constructor(page) {
this.page = page;
}
async gotoPage() {
await this.page.goto(yamlData.domain.naverWeather);
}
async weeklyListClick(){
// 'weeklyList'에 해당하는 모든 'li' 요소들이 화면에 보이는지 확인
await this.page.locator(weeklyList).isVisible();
// 'weeklyList'에 있는 모든 'li' 요소 가져오기
const dayButtons = this.page.locator(weeklyList).locator('li.item');
const count = await dayButtons.count(); // li 요소 개수 확인
// 각 요소를 순회하면서 클릭
for (let i = 0; i < count; i++) {
const dayButton = dayButtons.nth(i); // 각 li 요소 선택
await dayButton.click(); // 클릭
await this.page.waitForTimeout(1500); // 1.5초 대기
}
}
}
module.exports = NaverWeatherMain;
먼저 Main 클래스입니다. Main 페이지이기 때문에 메인 도메인으로 이동하는 goto 함수와 전국 날씨의 탭 버튼을 클릭하는 weeklyListClick 함수를 생성하였습니다.
보기에도 네이버 날씨의 "홈" 페이지 내부 항목에 해당하는 기능들만 들어가있는 상태입니다.
// NaverWeatherGnb.js 파일
const comparePageButton = 'div.menu_area >> a >> text="예보비교"';
const mainPageButton = 'div.menu_area >> a >> text="홈"';
const airPageButton = 'div.menu_area >> a >> text="미세먼지"';
const mapPageButton = 'div.menu_area >> a >> text="지도"';
const videoPageButton = 'div.menu_area >> a >> text="영상"';
const warningPageButton = 'div.menu_area >> a >> text="기상특보"';
class NaverWeatherGnb {
constructor(page) {
this.page = page;
}
async clickCompareMenuButton(){
this.page.locator(comparePageButton).click();
}
async clickMainMenuButton(){
this.page.locator(mainPageButton).click();
}
async clickAirMenuButton(){
this.page.locator(airPageButton).click();
}
async clickMapMenuButton(){
this.page.locator(mapPageButton).click();
}
async clickVideoMenuButton(){
this.page.locator(videoPageButton).click();
}
async clickWarningMenuButton(){
this.page.locator(warningPageButton).click();
}
}
module.exports = NaverWeatherGnb;
그리고 Gnb 파일입니다. 네이버 날씨에 어떤 페이지로 이동하여도 항상 사용할 수 있는 기능들을 모아뒀습니다.
상단 메뉴 이동 부분이 되며, 어떤 페이지에 있더라도 수행하면 정상 동작하는 기능들입니다.
// NaverWeatherCompare.js
const contentLayer = 'div#content';
class NaverWeatherCompare {
constructor(page) {
this.page = page;
}
async loadCheck(){
const isVisible = await this.page.locator(contentLayer).isVisible();
// 요소가 실제로 보이는지 여부를 출력
if (isVisible) {
console.log('The element is actually visible on the screen');
} else {
console.log('The element is not visible on the screen');
}
}
}
module.exports = NaverWeatherCompare;
예보 비교 페이지 입니다. 예보 비교 페이지 내부에서 수행하는 테스트가 아직 없기 때문에 간단하게 특정 div가 정상 표시되고 있는지 확인하는 함수만 추가하였습니다.
이처럼 각 페이지나 기능별로 분리한다면 테스트 스크립트를 구성할때 내가 원하는 코드만 호출하여 조립하면 하나의 테스트 시나리오가 완성됩니다.
//Main testcase
const { test } = require('@playwright/test');
const MainPage = require('../../naverPage/NaverWeatherMain');
const GnbPage = require('../../naverPage/NaverWeatherGnb');
const ComparePage = require('../../naverPage/NaverWeatherCompare');
let mainPage;
let gnbPage;
let comparePage;
test.beforeEach(async ({page}) => {
mainPage = new MainPage(page);
gnbPage = new GnbPage(page);
comparePage = new ComparePage(page);
await mainPage.gotoPage();
});
test('전국 날씨 탭 기능 확인', async ({ page }) => {
await gnbPage.clickAirMenuButton();
await page.waitForTimeout(2000);
await gnbPage.clickCompareMenuButton();
await comparePage.loadCheck();
await page.waitForTimeout(2000);
await gnbPage.clickMapMenuButton();
await page.waitForTimeout(2000);
await gnbPage.clickWarningMenuButton();
await page.waitForTimeout(2000);
await gnbPage.clickMainMenuButton();
await page.waitForTimeout(2000);
await mainPage.weeklyListClick();
await page.waitForTimeout(2000);
});
각 페이지 클래스에서 페이지 내에서 수행하는 기능들이 구현되어 있기 때문에 테스트 케이스에서는 현재 어떤 페이지에 위치해있는지만 생각하고 어떤 동작을 수행하게끔 할 것인지만 파악해둔다면, 단순한 호출로 하나의 테스트 자동화를 구현할 수 있게 됩니다.
그리고, 이렇게 페이지 모델링을 해 둔다면 나중에 페이지의 기능이 변경되는 경우, 특정 페이지 클래스의 식별자만 현행화 하거나 기능들을 수정한다면 테스트 케이스에서는 문제없이 돌아가기 때문에 유지보수 측면에서 큰 효과를 가져올 수 있습니다.
POM 구조에 대한 설명은 어느 블로그에 많기 때문에 실제 코드 위주로 설명하느라 부족한 부분이 있지만.. 기능 테스트 케이스를 작성할 때 POM 구조는 필수적인 부분이라 생각합니다. 나중에 유지보수 지옥을 맛보지 않기 위해선..!!
다음 포스팅에서는 Playwright의 주요 함수에 대해 알아보겠습니다.
'QA > 기능 자동화' 카테고리의 다른 글
기능 자동화 기술조사부터 환경 구성까지 (6) | 2024.11.04 |
---|---|
부족한 식별자로 신뢰도 있는 자동화 스크립트 구성하기 (1) | 2024.09.08 |
기능 자동화를 만들면 살충제 패러독스에 걸리지 않나요? (0) | 2024.07.04 |
Playwright를 활용한 자동화 연습 (1) | 2024.06.16 |
기능 자동화 동작 방식과 식별자 (0) | 2024.06.15 |