[jstest] selenium + nodeJS #2

개요

셀레늄은 브라우저에 인터렉션을 걸고 브라우저가 그것에 대한 반응을 하는지 감시하여 보고하는게 주 임무입니다. 따라서 다른 전구간 테스트 프레임웍이 그러하듯 관찰이나 대기에 대한 기능을 풍부하게 갖고 있습니다. 이번 글에서는 대기에 대해 알아보고 worker를 정리합니다.

암묵적 대기

우선 가장 기본적인 대기전략은 “무조건 뭘시켜도 일정시간 대기하면서 감시하기” 입니다. 이를 암묵적 대기(implicit wait)라 합니다. 이는 드라이버에게 설정할 수 있습니다.

const webdriver = require('selenium-webdriver'), By = webdriver.By, until = webdriver.until;
const chrome = require('selenium-webdriver/chrome');

const driver = new webdriver.Builder().forBrowser('chrome').build();
driver.manage().timeouts().implicitlyWait(3000);

인자는 밀리세컨으로 위의 예제는 3초를 매번 대기하면서 감시하게 됩니다. 당연히 이러한 여파로 대부분의 메소드는 thenable로 작동하게 됩니다.

driver.get(url);
driver.findElement(By.id('test1')).then(el=>console.log(el));

이제 위의 코드는 3초간 대기하면서 지속으로 폴링을 통해 test1에 해당되는 엘리먼트가 있는지 조사하게 됩니다. 하지만 그 전략 그대로 모든 것에 대기가 걸리기 때문에 굉장히 작동이 느려지게 됩니다(사실 없으면 없다고 곧장 판정하는게 대부분의 경우죠) 따라서 권장하지 않습니다.

명시적 대기

암묵적 대기와는 달리 처음부터 기대조건(ExpectedCondition)을 기술하여 작동시키는 명시적 대기용 메소드가 준비되어있습니다. 되도록이면 코드에서 의도가 드러나도록 이쪽 메소드를 사용하기를 권장합니다.

명시적 대기는 driver의 wait메소드를 사용합니다. wait메소드는

  1. 첫 번째 인자로 Promise나 Condition객체가 오고,
  2. 두 번째 인자로 폴링할 시간을 밀리세컨으로 지정합니다.

결과도 Promise이므로 then으로 받으면 됩니다. then의 resolve에게는 wait의 첫 번째 인자가

  1. WebElementCondition인 경우만 WebElement가 들어오고
  2. Promise, Condition인 경우는 driver가 전달됩니다.

하지만 매번 WebElementCondition이나 Condition객체를 만드는 것은 귀찮은 일입니다. 여러 경우에 사용하도록 미리 정의된 Condition을 생성해주는 편리한 until 라이브러리를 제공합니다. until을 이용해 간단히 명시적 대기를 걸어보죠.

const webdriver = require('selenium-webdriver');
const By = webdriver.By;
const until = webdriver.until; //until모듈

driver.get('http://...');

//명시적 대기
driver.wait(until.elementLocated(By.id('test1')), 3000).then(el=>console.log(el));

until을 이용하면 좀 간단히 기술할 수 있습니다. until이 제공하는 편의함수는 다음과 같습니다. 이름이 쉬워 별도의 설명은 생략합니다.

  • ableToSwitchToFrame(frame)
  • alertIsPresent()
  • elementIsDisabled(element)
  • elementIsEnabled(element)
  • elementIsSelected(element)
  • elementIsNotSelected(element)
  • elementIsVisible(element)
  • elementIsNotVisible(element)
  • elementLocated(locator)
  • elementsLocated(locator)
  • elementTextIs(element, text)
  • elementTextContains(element, substr)
  • elementTextMatches(element, regex)
  • stalenessOf(element)
  • titleIs(title)
  • titleContains(substr)
  • titleMatches(regex)
  • urlIs(url)
  • urlContains(substrUrl)
  • urlMatches(regex)

위의 until함수들은 죄다 편의함수로 내부에서는 new Condition이나 new WebElementCondition을 하고 있습니다. 이해하기 위해 직접 코드로 실현해보죠.

const wdMain = require('selenium-webdriver');
const By = wdMain.By;

//Condition과 WebElementCondition 정의
const wd = require('selenium-webdriver/lib/webdriver');
const Condition = wd.Condition;
const WebElementCondition = wd.WebElementCondition;

const worker = new chrome.Driver();
worker.get('http://...');

//titleIs를 직접 구현한 경우
worker.wait(
  new Condition(
    'titleIs를 직접 구현!',
    driver=>driver.getTitle().then(t=>t=='test1')
  ),
  5000
).then(d=>console.log('ok'));

//elementTextContains를 직접 구현한 경우
worker.findElement(By.id('test1')).then(
	el=>worker.wait(
		new WebElementCondition(
			'elementTextContains를 직접 구현!',
			driver=>el.getText().then(v=>v.indexOf('ab') > -1 ? el : null)
		)
	),
	5000
).then(el=>el.getText()).then(v=>reporter.report(v));

셀레늄의 노드용 코드는 es6기반으로 되어있고, 광범위하게 Promise를 사용하고 있어 이에 익숙치 않으면 좀 난감할 수 있습니다.
또한 javascript용 문서가 마땅치 않아 저 같은 경우 아예 깃헙의 코드로부터 사용법을 알아내고 말았습니다..=.=;

셀레늄깃헙링크

worker정리

본격적으로 내용을 전개하기 전에 여태까지 나온 내용을 바탕으로 worker를 정리하기 적절한 시점입니다. 다음과 같은 worker.js를 만들어보죠.

const wdMain = require('selenium-webdriver'), By = wdMain.By, until = wdMain.until;
const wd = require('selenium-webdriver/lib/webdriver');
const Driver = require('selenium-webdriver/chrome').Driver;

const DRIVER = Symbol();
module.exports = class{
	constructor(url){
		this[DRIVER] = new Driver();
		this[DRIVER].get(url);
	}
	element(css){
		return this[DRIVER].findElement(By.css(css));
	}
	elements(css){
		return this[DRIVER].findElements(By.css(css));
	}
	waitUntil(time, method, ...arg){
		return this[DRIVER].wait(until[method](...arg), time);
	}
	waitElement(time, title, f){
		return this[DRIVER].wait(new wd.WebElementCondition(title, f), time);
	}
	waitCondition(time, title, f){
		return this[DRIVER].wait(new wd.Condition(title, f), time);
	}
	by(method, v){
		return By[method](v);
	}
};

간단히 메소드를 살펴볼까요.

  • element, elements – 둘다 cssSelector를 인자로 받아 웹엘리먼트를 반환합니다. 예를들어 worker.element(‘#test1’) 처럼 쓰겠죠.
  • waitUntil – until을 이용한 조회는 worker.waitUntil(1000, ‘titleIs’, ‘test1’) 처럼 사용될 것입니다. 두 번째 인자가 until의 메소드명이죠.
  • waitElement, waitCondition – 직접 커스텀으로 wait를 구현하는 경우에 대응합니다.
  • by – 귀찮으니까 걍 내장시켰습니다. worker.by(‘id’, ‘test1’) 처럼 사용할 수 있습니다.

이를 통해 바로 앞에서 구현했던 until대기구문을 정리해보죠.

const Worker = require('./worker.js');
const worker = new Worker('https://www.bsidesoft.com/hika/wp/2196/test1.html');

worker.waitUntil(3000, 'elementLocated', worker.by('id', 'test1')).then(el=>console.log(el));

간단하게 처리되었습니다.

두 개의 커스텀 wait도 처리해보죠.

const Worker = require('./worker.js');
const worker = new Worker('https://www.bsidesoft.com/hika/wp/2196/test1.html');

worker.waitCondition(5000,
  'titleIs를 직접 구현!',
  driver=>driver.getTitle().then(t=>t=='test1')
).then(d=>console.log('ok'));

worker.element('#test1').then(
  el=>worker.waitElement(5000,
	'elementTextContains를 직접 구현!',
	driver=>el.getText().then(v=>v.indexOf('ab') > -1 ? el : null)
  )
).then(el=>el.getText()).then(v=>reporter.report(v));

여러모로 코드가 줄어들고 직관성이 높아졌습니다.

결론

관찰프레임웍의 필수적인 대기에 대해 알아봤습니다. #3 포스팅에서는 각종 이벤트처리를 알아보겠습니다. #1부터 따라오신 분들이라면 현재 적어도 작업폴더에 chromedriver.exe, reporter.js, worker.js, test.js 이렇게 네개의 파일은 존재하시는게 정상입니다 ^^

%d 블로거가 이것을 좋아합니다: