[jstest] selenium + nodeJS #1

인수테스트

웹솔루션의 경우 실제 환경에서 파일업로드 및 ajax연동을 포함한 다양한 브라우저로 산출물을 테스트하는 것을 인수테스트로 볼 수 있습니다. 간단히 말해 최종 유저가 실제 사용할 때 버그가 발생하는지 아닌지를 테스트해보는 것입니다.
테스트이론에 대한 장황한 설명은 이미 수 많은 문서와 책이 다루고 있으며 차차 저도 다른 포스팅을 통해 조금은 설명할 예정입니다만 이번 시리즈에는 순수하게 셀레늄을 노드로 활용하는 방법과 셀레늄API를 정리하는 정도를 목표로 하고 있습니다(총 3회정도 분량으로 예상 중입니다)
이번 포스팅은 첫 입문과정을 다룹니다.

  • 이전에 언급드린대로 이제 비사이드블로그는 es6로 포스팅합니다…죄송합..

nodeJS용 셀레늄설치

아래 링크에 따라 설치를 진행합니다.

http://seleniumhq.github.io/selenium/docs/api/javascript/index.html

간단히 짚어보면

  1. nodeJS 설치되어 있어야 함
  2. javaSDK 설치되어 있어야 함
  3. npm install selenium-webdriver 설치
  4. 각 브라우저별 드라이버 다운로드
  5. test.js를 만들어 Usage에 있는 내용 카피해서 노드로 실행!

굉장히 쉽기 때문에 따로 설명하지 않습니다만 한가지 조심하실건 각 브라우저별 드라이버를 다운로드 받아서 test.js와 같은 위치에 카피해두셔야한다는 정도입니다.

간단한 셀레늄 구동 골격

최소한으로 작동하는 js코드를 작성해보죠.
우선 웹드라이버와 크롬브라우저를 불러오고 이를 통해 실제 사용할 드라이브까지 초기화합니다.

const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const driver = new webdriver.Builder().forBrowser('chrome').build();

그닥 뭐하는 코드인지 고민하지 마세요. 차차 복잡한 사용은 익히면 그만입니다. 그저 저 세줄이 적힌 그대로 웹드라이버와 필요한 크롬브라우저용 드라이버, 그리고 실제 제어하게될 드라이버를 생성했다는 것만 알면 됩니다.

이제 driver를 이용해 다양한 모험을 떠나볼텐데, 최초로 해야할 일은 드라이버에게 특정 웹사이트를 열게 하는 일이겠죠. 이는 get, post등의 메소드를 통해 가능합니다. 우선 테스트할 간단한 test.html을 작성해보죠.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>test1</title>
</head>
<body>
  <div id="test1">abc</div>
</body>
</html>

문서 내부에는 test1이라는 아이디로 div가 하나 간단히 들어있습니다. 이걸 적당한 서버에 올리고 해당 url을 driver가 로딩하게 해보죠.

const url = 'https://www.bsidesoft.com/hika/wp/2196/test1.html';
//html을 업로드한 경로에서 불러온다
driver.get(url); 

그럼 크롬이 뜨고 그 크롬에서 우리가 지정한 주소를 불러오는게 보여야 정상입니다. 이제 브라우저 띄우고 특정 url 로딩하는건 되었으니 로딩된 페이지에서 뭘할까를 연구해봐야죠. 로딩된 페이지의 특정 엘리먼트를 얻어 그 엘리먼트의 값을 확인하는 것부터 해보죠.
이는 웹드라이버의 By라는 모듈을 이용하면 됩니다. 우선 아까 로딩한 웹드라이버로부터 By을 정의합니다.

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

이를 이용해 드라이버 findElement메소드의 인자로 넘겨줄 수 있습니다.

const el = driver.findElement(By.id('test1'));

이렇게 얻어진 엘리먼트는 DOM객체가 아니라 셀리늄객체로 WebElement라는 녀석입니다. 우선 이렇게 얻은 WebElement의 내용이나 확인해보죠.

el.getText().then(v=>console.log(v));
  • 기존 예제가 동기적으로 되어있어서 프라미스기반으로 변경했습니다.

getText()는 내부의 내용을 단일한 text로 묶어서 제공하는데 innerText와 비슷하게 text로 병합하여 나온다고 생각하시면 됩니다. 앞서 예제로 만든 html상의 div내에는 abc가 있었으므로 abc가 출력됩니다. 여기까지의 코드를 모아서 흐름을 느껴보죠.

//웹드라이버초기화
const webdriver = require('selenium-webdriver');
const By = webdriver.By;
//사용할 브라우저드라이버초기화
const chrome = require('selenium-webdriver/chrome');
//드라이버초기화
const driver = new webdriver.Builder().forBrowser('chrome').build();
//드라이버사용
const el = driver.findElement(By.id('test1')); //엘리먼트를 얻고
console.log(el.getText()); //무언가 함

이 기본 흐름을 이용하여 다양한 작업을 수행하고 그 결과를 보고 하게 됩니다.

By를 이용한 엘리먼트 얻기

driver는 하나의 엘리먼트를 얻기 위한 findElement와 여러 개의 엘리먼트를 리스트로 얻는 findElements 메소드를 제공합니다.
이 메소드가 받아들이는 인자가 By의 메소드 호출결과입니다. 따라서 By객체를 잘 이용하는 것이야말로 테스트할 엘리먼트를 얻는 첩경입니다.
By에는 다음과 같은 주요한 메소드가 있습니다.

  • By.id(id)
  • By.name(name)
  • By.className(class)
  • By.tagName(tagname)
  • By.linkText(A태그의 text값)
  • By.partialLinkText(A태그의 일부 text값)
  • By.cssSelector(selector)
  • By.xpath(xpath)

직관적인 메소드명으로 되어있어 별 설명은 필요없을 것입니다. findElement는 driver에만 존재하는 것이 아니라 WebElement에도 존재하므로 다음과 같은 두 가지 스타일로 체이닝할 수 있습니다.

const div = driver.findElement(By.id('test1'));
const span = div.findElement(By.tagName('span'));
//or
const span1 = driver.findElement(By.id('test1')).findElement(By.tagName('span'));

WebElement조작

자 이제 문서에서 원하는 엘리먼트는 얻었으니 이 녀석에게 get하거나 set하는 일이 있겠죠. 대표적인 메소드를 살펴봅니다.

  • clear() – innerHTML = ” 과 같은 효과를 일으킵니다.
  • click() – 해당 엘리먼트를 클릭합니다.
  • findElement(By..) – 자손엘리먼트 중 하나를 가져옵니다.
  • findElements(By..) – 자손엘리먼트 여러개를 배열로 가져옵니다.
  • getAttribute(속성명) – 속성값을 가져옵니다.
  • getCssValue(속성명) – 속성에 해당되는 스타일값은 계산된 값입니다.
  • getLocation() – 웹페이지 위치상의 x,y를 나타내는 포인트를 반환합니다.
  • getRect() – Rectangle 구조로 width, height를 포함하여 반환합니다.
  • getSize() – Dimension 구조로 width, height를 반환합니다.
  • getTagName() – 태그명을 반환합니다(소문자로)
  • getText() – innerText값을 가져옵니다.
  • isDisplayed() – 화면에 표시된 것인가 아닌가 판정합니다.
  • isEnabled() – 보통 input계열의 태그가 enable상태인지 판정합니다.
  • isSelected() – 선택된 상태인지를 반환합니다.
  • sendKeys() – 해당 태그에 키보드 입력을 실시합니다.
  • submit() – form인 경우 submit을 실시합니다.

직접적인 set계열이 없는 이유는 셀레늄의 목적이 웹사이트를 구축하는 것이 아니라 유저를 대신하여 웹을 사용하는 것이기 때문입니다. 따라서 직접 엘리먼트에 set을 하는게 아니라 이벤트(sendKeys, click)를 일으키게 됩니다.

일단 이 정도만 갖고도 굉장히 여러가지 테스트를 수행해볼 수 있습니다. 하지만 앞으로의 다양한 테스트와 결과를 보다 쉽게 보기 위해 처음부터 약간의 기반구조를 구축해보죠.

출력용 driver의 구성

두 개의 driver를 운영하는 방식을 생각해보죠.

  1. 드라이버 한 개는 테스트하려는 사이트에 들어가서 다양한 과업을 수행하는 목적으로 사용하고
  2. 또 다른 드라이버를 이를 표시할 웹사이트에 지속적으로 과업결과를 레포트해주는 목적으로 사용하는

시나리오 입니다. 편의상 1번을 worker라 부르고 2번을 report라고 부르겠습니다.
우선 매우 간단하게 report.html을 만들어보죠.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Report</title>
</head>
<body>
  <ol id="report"></ol>
</body>
</html>

report라는 간단한 태그가 생겼으니 여기에 뭔가 보고하면 될 것입니다. 이를 웹서버에 올려 주소를 얻으면 레포트의 준비는 끝입니다.
마침 아까 test1.html가 있으니 이제 worker가 테스트할 결과를 report에 보고하게 해봅니다.

//웹드라이버, By, 크롬드라이버 준비
const webdriver = require('selenium-webdriver'), By = webdriver.By;
const chrome = require('selenium-webdriver/chrome');

//워커와 레포트 준비
const worker = new webdriver.Builder().forBrowser('chrome').build();
const report = new webdriver.Builder().forBrowser('chrome').build();

//레포트의 준비
const reportUrl = 'https://www.bsidesoft.com/hika/wp/2196/report.html';
report.get(reportUrl);

//테스트시작
const testUrl = 'https://www.bsidesoft.com/hika/wp/2196/test1.html';
worker.get(testUrl);
worker.findElement(By.id('test1')).getText().then(
	v=>report.executeScript(
		`document.getElementById('report').innerHTML += "<li>${v == 'abc'}</li>";`
	)
);

여기서는 아직 언급되지 않았던 드라이버의 메소드인 executeScript를 사용했습니다. 이는 해당 드라이버에게 인자로 받아온 문자열을 스크립트로 실행해주는 기능입니다. 이를 통해 해당 값이 abc인지를 판정한 결과를 포함시키도록 처리했습니다. 실행하면 자동으로 두개의 크롬브라우저를 띄워서 report측에 true가 표시되게 됩니다.

Screenshot_1

report 함수화

향후의 지속적인 사용과 정리를 위해 report부분을 별도로 빼서 재활용할 수 있게 꾸미죠. reporter.js를 작성해봅니다.

const wd = require('selenium-webdriver');
const ch = require('selenium-webdriver/chrome');
const DRIVER = Symbol(); //private용 심볼
module.exports = class{
	constructor(url){//보고용 url지정
		this[DRIVER] = new wd.Builder().forBrowser('chrome').build();
		this[DRIVER].get(url);
		Object.freeze(this);
	}
	report(value){//실제보고하기
		this[DRIVER].executeScript(
			`document.getElementById('report').innerHTML += "<li>${value}</li>";`
		);
	}
	quit(){//종료시킴
		this[DRIVER].quit();
	}
};

이제 test.js는 간단히 사용할 수 있습니다. 변경된 코드를 보죠.

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

//레포터준비
const Reporter = require('./reporter.js');
const reporter = new Reporter('https://www.bsidesoft.com/hika/wp/2196/report.html');

//테스트시작
const worker = new webdriver.Builder().forBrowser('chrome').build();
worker.get('https://www.bsidesoft.com/hika/wp/2196/test1.html');
worker.findElement(By.id('test1')).getText().then(v=>reporter.report(v == 'abc'));

훨씬 간단하게 사용할 수 있게 되었습니다. 향후 Reporter의 기능을 늘려가면서 차근차근 테스트 프레임웍이 되어갈 것입니다.

결론

셀레늄을 활용하는데 있어 가장 쉬운 nodeJS버전을 살펴봤습니다. 다음에는 보다 복잡한 환경에 대한 구체적인 사례를 살펴보면서 동시에 Reporter를 더욱 발전시켜가보도록 하죠.

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