안녕하세요 깍돌이 입니다. 오늘은  간단한 트러블 슈팅 입니다.

폐쇠망 서버에 Node.js Agent를 심어야 하는 경우가 있는데 해당 서버에 새롭게 설치를 하려고하면

Node.js 설치 파일 및 Agent 코드를 심어 놓습니다. ( node_modules 포함 ) 

 

기존에 서버들은 노드 버전이 13 14 버전을 사용했는데 이번에 새로 하는 노드로 설치시 내부 의존성으로 설치하여 실행하던 PM2 명령어들이 먹지 않는 현상이 발생했습니다.

 

결론은 맨밑에 있습니다.

Node.js 버전

[root@private-inter-comm-send transaction-test]# node -v
v16.14.0
[root@private-inter-comm-send transaction-test]# npm -v
8.3.1
[root@private-inter-comm-send transaction-test]#

package.json

"scripts": {
    "dev": "nodemon app.js",
    "start": "pm2 start app.js --name 'ServerNode'",
    "nacl": "pm2 start app.js --name 'ServerNode' -- nacl",
    "udp": "pm2 start app.js --name 'ServerNode' -- udp",
    "list": "pm2 list",

 

기존에 사용하던 명령어는 npm run start 

 

[root@private-inter-comm-send transaction-test]# npm run start

> servernode@0.0.1 start
> pm2 start app.js --name 'ServerNode'

sh: pm2: command not found
[root@private-inter-comm-send transaction-test]#

 

sh: pm2: command not found

분명히 깔려는 있는데 연결이 안되는거 같습니다.

내부 폴더의 ./node_modules/pm2로 정상실행되는 모습입니다.

 

 

정상적으로 실행되는 경우

 

node랑 npm 버전이 다른거는 큰차이가 없을거 같아서 심링크를 확인해보겠습니다.

 

연결 문제는 아닌거같네요

 

package-lock.json 확인시

 

 

비정상 package-lock.json 을 보니 node_modules가 붙어있는데 

 

npm audit fix 

npm audit 으로 꼬여져있는 부분들을 재 설치 후 다시 말아서 이동

별효과 없음

 

이외에 별짓 다해봤는데

 

노드 16버전에 문제가 있다고 생각되어서 노드 14.19 마지막 버전으로 내리고 정상화되었습니다.

 

내부적으로 노드 14버전을 LTS로 가지고 작업해야겠네요

 

다시 정상화 되었네요;

 

 

14.19 버전으로 당분간 사용하는것으로 하겠습니다.

 

 

 

백업 대상 레포 주소 A

 

 

백업을 하기 위해 새로 만들어 놓은 레포 주소 B

 

 

git clone --mirror A

cd A

git remote set-url origin B

git push --mirror

 

 

안녕하세요 깍돌이입니다.

 

웹 자동화를 하기 위해서 사용되어지는 waitForNavigationAPI 랑 isIntersectingViewport API 에 대해서 간단 포스팅을 진행하려고 합니다.

 

상세 포스팅은 자동화 툴에서 따로 진행 하려고합니다. ( 공식 홈페이지 참조 및 관련 샘플 까지 ) 

이번 설명은 제목과 같이 Naver Cloud Platform 에 있는 웹을 샘플로 설명 드리겠습니다.

 

우선 간단하게 MPA와 SPA에 대해서 알고 가야 합니다.

 

아래 velog.io 에 있는 간단 설명 출처 입니다.

https://velog.io/@thms200/SPA-vs.-MPA

 

SPA vs. MPA

SPA, MPA란 무엇일까? 차분히 정리해보자. 1. 정의 글자 그대로 단순(?)하게 해석하면, SPA(Single Page Application)는 한 개(single)의 Page로 구성된 application 이고, MPA(Multi Page application)

velog.io

 

간단하게 핵심 요약 을 하자면 아래의 그림입니다.   ( 조금 수정해서 말씀드리고 싶지만 )

다른 부분은 제외하고 Page Reload 가 되는 시점을 봐야합니다.

SPA , MPA 설명함에 있어 많이 생략될수있는 점 양해 부탁드립니다.

MPA에서 보시면 유의미하게 봐야 할 부분이 렌더링 및 페인트 ( 화면 그리기 ) 는 브라우저에서 발생합니다.   하지만

MPA는 Element ( 브라우저의 syntax tree 중 HTML Element ) 를 미리 서버에서 만들어 놓은 후 클라이언트에게 던져주는 것익오 SPA의 경우 Element를 서버에서 그려주는것이 아닌 클라이언트(브라우저) 에서 JS를 실행하여 만들고 그다음에 브라우저가 렌더링을 하는 방식입니다. ( syntax tree에서 렌더링을 하는 부분은 매우 복잡하고 내용이 길어지기 때문에 생략 합니다. ) 

 

요약을 하자면 MPA는 밑그림을 백엔드에서 그려서 던져주고 SPA는 밑그림 부터 내가 그려야 합니다.    

(그렇기 떄문에 Performance 탭에서 확인시 JS의 사용시간이 SPA가 확연이 긴것이 확인됩니다. ) 

 

MPA = 다음카페

 

로드 후 기본적인 스크립팅 후 렌더 및 페인트가 이루어집니다.

 

SPA = React 공식 홈페이지 의 블로그 커뮤니티 탭 ( 홈은 Code 를 보여주는 부분이 좀더 무거움 )

 

두개의 차이를 느끼셨나요?

 

다음카페의 경우 Dotax라는 카페의 게시판이기 때문에 글에 대한 데이터 및 Pagination 등 꽤 복잡하게 이루어져있는 페이지에 비해 리액트 공식홈페이지의 블로그 커뮤니티 탭의 렌더링은 JS 의 위주로 준비 및 세팅이 되고있습니다. 

 

여기서 이제 유의 해야 할 점이 있습니다.

waitForNavigation API

우선 공식 puppeteer github에 있는 waitForNavigation 의 자료 설명입니다.  아래의 사용 예시까지 잘되어있습니다.

 

navigation 이 완료된 후  -> 이부분이 핵심인데요 navigation 이 완료되는 조건이 중요합니다.

해당 API 의 프로퍼티들을 보면 load, documentloaded ( dcl 이라고도합니다. )  이부분들이 의미하는 바는 브라우저의 Document ( 문서 ) 의 로드가 어디까지 되었느냐에 따라 체크를 할수있도록 되어있습니다. 

한창 JS , Browser , HTML , CSS등 초반에 공부하면 무조건 나오는 내용 중 하나인데요 좀더 깊어지면 

FP ( First Paint )
FCP ( First Contentful Paint ) 
DCL (DocumentContentLoad)
L (Load ) 
LCP (Largest Contentful Paint )
FMP ( First Meaningful Paint ) 

등 다 튀어나올수 있습니다 . 포스팅은 최대한 짧게 하려고 하니 내용은 생략하지만  핵심만 딱 설명이 쉽지 않네요 ㅎ;;

 

여튼! 위의 옵션을 보았을때 L, DCL , networkidle 0 같은걸로 보아 브라우저의 network 이벤트 타임  혹은 문서 로딩 을 기준으로 체크 하는 점을 볼수 있습니다.   

 

그럼 여기서 중요한 점이 뭐냐면  해당 옵션 및 이벤트 타임은 새롭게 그려질때 만 발생한다는 점입니다.

네이버 클라우드 플랫폼을 기준으로 하고 있으니 예시를 들어 드리자면

로그인창에서 -> 콘솔창으로 넘어갈때는 사용하여도 오류가 나지 않습니다. 콘솔창으로 넘어가기 때문 (새롭게 렌더링 )

하지만 서버를 생성 하는 부분

 

 

여기 페이지인 server/create 페이지부터는 서버 생성을 하기 위해 이미지를 선택하고 다음> 버튼을 눌러도 위에 navigation API 를 사용시 무조건 timeout이 발생합니다.     

이유를 다시 말씀드리면 위의 서버 페이지의 주소는 아래와 같습니다.

https://console.ncloud.com/vpc-compute/server/create

그리고 이미지를 선택하여 다음을 선택시 나오는 화면 및 링크는 아래와 같습니다.

링크 : https://console.ncloud.com/vpc-compute/server/create

화면 :

그렇습니다. 화면은 변경되었지만 ?  URL은 변경되지않았습니다.  리액트 홈페이지의 경우 React의 Router를 사용하여서 화면을 렌더링 해주기때문에  URL 이 변화가 생기면서 렌더링을 하고있지만 해당 홈페이지도 아마 확인시 새로운 networkidle을 받아 오지는 않을것으로 보입니다. ( 하지만 SSR을 적용하였다면 받아 올수 있습니다. ) 

그렇기 때문에 새로운 DCL 이나 L, FMP  ( 아주 대중적으로 jquery 쓰시는 분들은 jquery.ready 도 있습니다. ) 등이 새롭게 이벤트가 캐치 되지 않기 때문에 waitForNavigation 는 매우 높은 확률로 timeout이 나고 이부분을 모르는 puppeteer 사용자는 의문을 표하게 되며 무조건 timeout을 스택오버플로우에 검색하게 될수 있습니다.

 

그럼 UI 자동화 툴을 이용하여서 페이지를 넘기거나 다른 작업이 시작전 정상적으로 로드 됨을 체크하기위해서 해당 API 를 썻던 경우 위의 화면과 같은 CSR의 경우 어떻게 확인을 하는것이 좋을까 에 대한 값은 아래의 API 가 있습니다.

 

 

isIntersectingViewport API

공식 홈페이지부터 보고 가시겠습니다~!

returns : 요소가 뷰 포트에 표시되면 true를 반환합니다. 

 

여기서 뷰 포트는 현재 브라우저의 뷰 라고 할수 있습니다.

 

그럼 이걸로 페이지 가 정상 렌더 됐는지 어떻게 확인할까에 대해서 위의 서버 생성 페이지를 한번 보겠습니다.

 

상단 UL 부분

 

하단 페이지 렌더 부분

 

 

여기서 특이점을 찾으셨나요?

 

4개의 DIV가 있고 현재 보여주는 1개 제외하곤 display :none을 통해서 처리하고있습니다.

 

? 위에 5개가있던데 1개는 어디있을까 하고 확인해보니

 

마지막 생성시에 붙는점이 확인되었습니다. (의도는 잘 모르겠습니다.)

 

그럼 이제 이 페이지에서 내가 서버를 선택하고 -> 서버 생성 페이지로 정상적으로 들어왔는지를 체크해야합니다.

 

일단 2가지가 있는데 isIntersectingViewport 를 먼저 실행하진않습니다.  이유가 뭐냐면 저기 위에있는 html , div, ul 등을 HTML 태그라고 하면서 Element라고 표현하기도합니다. 그리고 이 요소들이 붙는 부분이 DOM 인데 이 Element들이 DOM 에 붙지 않았다면 당연히 브라우저는 렌더링을 해주지 않기 때문에 

 

우선적으로 DOM 에 Element가 붙었는지를 체크합니다. 체크를 하는 Element를 선택하는 기준은 모두가 다르지만 저는 다음에 액션을 진행할 Element를 기준으로 잡습니다.  DOM 체크는 간단합니다.

Puppeteer는 단일 요소는 $ 다중 요소는 $$ 로 찾게 되는데 해당으로 체크할시 null 이 아니라면 Element가 DOM 에 는 붙어있다는 의미가 됩니다. 

 

그리고 그다음 isIntersectingViewport 를 호출하면 현재 보여지는 화면에 제가 선택한 Element 가 보여지는지 안보여지는지 확인이 가능합니다.

 

 

모든 페이지가 MPA로 구성되어있다고 해도 현재 브라우저의 뷰 포트에서 노출이 되고있느냐 아니냐로도 체크 되기도 하기 때문에 해당 API 는 지금 보여지는가 에 대한 사용으로 가능할거 같습니다.

 

 

 

감사합니다.

 

안녕하세요 깍돌이 입니다.

 

기존에 준비하였던 내용중 일단 여기까지 하고 배포 릴리즈가 있기 때문에 .. 주말에 작업을 (또르르) 하고 다시 이어서 할 예정입니다.

 

품질은 제품이 잘 나가야 하는게 1순위니까요 이번 LNB Menu 선택 이후에 할 예정된 포스팅은 

  • Selenium-Webdriver 에서 Puppeteer 로 작업중인 이유 (Puppeteer  선택의 이유 )
  • 요즘 새로 핫한 playwright 에 대해 ( 퍼펫티어 팀이 나가서 MS에서 개발 중 ) 
  • MPA, SPA 페이지에 대한 waitForNavigationAPI , isIntersectingViewport API  사용 
  • 현재 진행 중 인 Console 자동화에서 Server 생성 (waitForNavigation API 먼저 포스팅 예정)
  • Server List에서 서버 상태 체크 

등등이 있고 이제까지 업무하면서 개인적으로 공부 했던 부분들을 모두 포스팅으로 남겨 놓도록 하겠습니다.

 

LNB Menu 선택을 위한 함수 제작 입니다.

뭔가 앞에 로그인기능, 콘솔 이동, 파라미터 유효성 등 저렇게까지 해야하나 싶을정도의 예외 처리 등이 들어가게 되는데 이유는  공통적으로 사용되는 기능이라면 모듈화가 필수기 때문입니다. 

 

3개의 Test Case 가 아래처럼 되어있다고 생각해보겠습니다.

 

각각의 로그인을 따로 구현, 콘솔이동도 따로 구현, 메뉴선택도 따로 구현 하였습니다.

이렇게 구현할 경우면 앞선 포스팅처럼 까다롭게 할필요도 없습니다. 단순히 그냥 지금 현상태로 로그인 Element가져와서 바로 입력하고 실행하게 하면 되는 것이지요  그럼 예외처리도 필요없고 파라미터도 필요 없고 하지만 대부분의 케이스들의 UI를 하기 위해선 콘솔로 이동을 하여야 하기 때문에 중복되는 케이스들이 발생합니다. 

 

그래도 사용성에 문제가 없어서 잘 쓰고 있었다면 ?  네 이상이 없으면 상관이없을 수 있습니다. 

하지만 이렇게 진행하다가 Automation Test Case가 100건까지 늘어났습니다. (이때까지 아무 이슈가 없다고 했을 경우)

그러는 와중에 개발팀에서 메뉴 Item의 class를 바꿨습니다.  그럼해야할 작업은

 

"모든 케이스의 직접 .class를 다시 맞춰서 찾아 주는 작업을 해줘야 합니다.  콘솔이동시에 URL체크로 하고있는데 이경우도 마찬가지입니다. 이동하는 URL의 변경 될경우 모두 바꿔 주어야 합니다.  그렇기 때문에 공통되는 작업들에 대해서는 일단 혼자 작업한다고 해도 모듈화를 우선적으로 하는게 건강에 좋기 때문에 위와같이 작성 되고있습니다.

그리고 모듈화에 앞서 UI Automation 에 서 보면 아래와 같은 질문들이 올라오는 경우가 있습니다.

 

"UI Automation 을 하면 개발자들의 자꾸 바꾸는 바람에 테스트가 오류가 납니다."

 

라는 이야기를 많이 듣기도했고 해보다 보니 그런거 같기도 하고 그래서 UI 는 답이없네 라고 할수도 있습니다.

하지만 실제로 경험을 해봤을 경우 웹사이트의 경우 그렇게 크게 바뀌지 않습니다. (대개편이 일어나지않는 이상)

특히 B2B 의 제품을 QA하고 계시다면 더더욱 그럴일은 더 적어지게 됩니다.  

 

"그럼 QA분들이 말하는 게 다 허상이냐" 그건 또 아닙니다. 제가 느꼈던 짤막한 경험으로는 xpath 의 사용 혹은 고정 CSS Selector의 사용이였습니다. ( Element 선택에 대한 기본기가 부족하다고 할수 있습니다. )

 

예를 들면 요소 > 요소 > 요소  에서 최초에 3번째의 요소만 무조건 찾는 형태로 구성되어 있는 상태에서 

개발자들의  hidden Element를 추가하게 될시 ( Tooltip 등을 Custom 으로 제작시등 많이 사용 합니다. - Z-index를 통한 툴팁 박스 ) 

요소 > 요소 > 히든 > 요소 의 구조가 가 되기때문에 기존에 지정해놓은 경로인 세번째 요소로는 찾아지지가 않는 것뿐입니다. xpath 가 나쁘다는 건 아니지만 CSS Selector를 유연하게 작업할수 있다면 그리고 공통적으로 써야하는 모듈을 제작하면서 예외처리를 하나씩 추가하게되면 화면을 통쨰로 갈아 엎지 않는 이상 자주 변경할 일은 발생하지않습니다.

 

그래서 결론적으로는 해당 공통케이스에 대한 모듈화 + CSS Selector를 위한 정확한 Element 선택 을 LNB Menu 에서 사용하고자 합니다.   같이 사용하게된다면 메뉴는 가장많이 사용되는 기능일수 있습니다.

 

 

 

 

 

 

우선 사용 호출 부 입니다.

navigateLNBMenu 호출시 

해당 서버 밑의 서버 메뉴를 선택해야 하기 때문에 Server, Server를 파라미터로 받습니다. 만약에 3depth 까지 있을수 있기에 파라미터는 배열형태로 받게 되었습니다.

 

["Server","Storage"]  라면 Server를 열고 Storage를 선택하게 합니다.

 

지금은 완성이 아니지만 추가작업으로 Server가 ( 1depth ) Open 되어있는상태에서 Server, Snapshot이 들어오게된다면

1depth의 Server가 Expand 상태라면 바로 Snapshot을 선택할 수 있어야합니다.

 

 

 

 

다른 파라미터로는 testplatform 과 environment를 받게 됩니다.  NCP의 콘솔의 경우 Classic 과 VPC를 선택할수 있기 때문이고 environment 는 환경 로깅 용 입니다.

보시면 해당 페이지의 개발자의 경우 모든 첫번째 메뉴들은 a태그와 id값을 사용 하였습니다.

따로 요청 하지않은 경우에 위와같이 id값을 직접 쓰게 되면 매우 좋은 케이스로 자체적으로도 유니크한 값을 쓰고있다고 보시고 #을 이용하여서 가져옵니다.

 

#인 경우는 따로처리할 필요가 없습니다. 다만유의해야 할 점이 있다면 li나 a 태그가 아니라 그 하단에 span 태그를 선택하는점  '#Server > span' 으로 클릭해주면 자연스럽게 열리게 됩니다. 

 

DNS 메뉴를 선택하려고 해도 

 

같은 방식으로 되어있기 때문에 첫번째 메뉴 선택까진 큰 무리가 없습니다.

 

두번째 서브 메뉴를 선택하려고 하면  메뉴상태에 따라서 여러가지 선택이 될수 있는 데 금일은 .active상태인걸 확인하고 (바로 1depth 의 메뉴가 열렸다는 가정) 선택을 할수 있도록 합니다. 

 

지금 작성을 하려고 보니 예외처리나 여러가지 기능들이 잘 안되어있네요.  간단하게 설명드리자면

 

위의 케이스는 active된 케이스 ( Server ) 에 첫번째 Element를 가져와서 선택하게 되어있네요 (이부분은 수정해서 해당 포스팅에 그대로 작성하겠습니다.)

 

상태별 케이스를 추후 추가!

 

 

 

 

 

 

 

 

안녕하세요 Software QA로 일하고 있는 깍돌이 입니다.

해당 포스팅 시리즈는 NCP 를 기준으로 UI Automation 을 하는 과정기를 하나씩 포스팅 하려고 합니다.

모두가 알고 있는 사이트를 기준으로 하는게 좋을거 같아서 선정하게 되었습니다.

 

기존엔 네이버로 했었는데 트래픽이 많이 몰려서 그런가 IP를 막고 그러네요 ㅜㅜ 우회 하는 방법은 찾았지만

 

포스팅으로 써서 좋을게 없을거 같아서 요즘 클라우드 시대에 많은 사람들이 사용하기 시작하는 네이버 클라우드 플랫폼을 기준으로 작성하게 되었습니다.

 

우선 UI Tool 은 구글에서 관리하고 요즘 핫한 Puppeteer로 작성하겠습니다.

기본적인 툴 세팅이나 실행등은 많은 블로그 및 오픈소스 Github에 너무 자료 설명이 잘되어있어서 너무 한땀 한땀은 스킵을 하도록 하겠습니다.~


Puppeteer - Github ( Example 및 API 리스트들 다 있어서 좋습니다. )

https://github.com/puppeteer/puppeteer

 

GitHub - puppeteer/puppeteer: Headless Chrome Node.js API

Headless Chrome Node.js API. Contribute to puppeteer/puppeteer development by creating an account on GitHub.

github.com

 

 

환경 세팅

기본적으로 Node.js  + Type Script + Puppeteer 환경을 기준으로 하기 때문에 기본적인 환경은 작성 하도록 하겠습니다.

package.json

  "dependencies": {
    "axios": "^0.21.1",
    "cloudscraper": "^4.6.0",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "folder-logger": "^1.0.9",
    "form-data": "^2.3.3",
    "moment": "^2.29.1",
    "multer": "^1.4.2",
    "puppeteer": "^5.0.0",
    "puppeteer-extra": "^3.1.12",
    "puppeteer-extra-plugin-stealth": "^2.4.12",
    "request": "^2.88.2"
  },
  "devDependencies": {
    "@types/axios": "^0.14.0",
    "@types/dotenv": "^8.2.0",
    "@types/form-data": "^2.5.0",
    "@types/jest": "^26.0.20",
    "@types/moment": "^2.13.0",
    "@types/multer": "^1.4.5",
    "@types/node": "^14.14.28",
    "@types/puppeteer": "^5.4.3",
    "pm2": "^4.5.4",
    "typescript": "^4.1.5"
  }

tsconfig.json

 

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist",
    "baseUrl":".",
    "paths":{
      "*":[
        "types/*",
        "bin"
      ],
      "dist/*":[
        "../../dist/*"
      ]
    },
    "noEmitHelpers":false,
    "importHelpers": false, // tslib 에서 방출된 헬퍼를 import 합니다. (예. __extends, __rest, 등..)
    "sourceMap": false, //해당하는 .map 파일을 생성합니다.
    "declaration": true,  //해당하는 .d.ts 파일을 생성합니다.
    "inlineSourceMap": false, //별도의 파일 대신 소스 맵으로 단일 파일을 내보냅니다
    "noImplicitAny": false, // any 타입으로 암시한 표현식과 선언에 오류를 발생시킵니다.
    "module": "commonjs",
    "target": "es5",
    "types": [
      "node"
    ],
    "lib": [
      "ES5","ES6","DOM","DOM.Iterable"
    ],
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true, //동일 파일 참조에 대해 일관성 없는 대소문자를 비활성화합니다.
    "noImplicitReturns": true, // 함수의 모든 코드 경로에 반환값이 없을 때 오류를 보고합니다.
    "noImplicitThis": true, //any 타입으로 암시한 this 표현식에 오류를 보고합니다.
    "strictNullChecks": true, // 엄격한 null 검사 모드에서는 null과 undefined 값이 모든 타입의 도메인에 있지 않고 그 자체와 any만 할당할 수 있습니다(한 가지 예외사항은 undefined 또한 void에 할당 가능하다는 것입니다).
    "suppressImplicitAnyIndexErrors": true, //인덱스 시그니처가 없는 객체를 인덱싱하는 경우 --noImplicitAny 억제합니다. 오류를 시그니처 자세한 내용은 #1232 이슈를 참조하세요.
    "allowSyntheticDefaultImports": true,  //default export가 없는 모듈에서 default imports를 허용합니다. 코드 방출에는 영향을 주지 않으며, 타입 검사만 수행합니다.
    "esModuleInterop": true,// 런타임 바벨 생태계 호환성을 위한 __importStar와 __importDefault 헬퍼를 내보내고 타입 시스템 호환성을 위해 --allowSyntheticDefaultImports를 활성화합니다.
    "resolveJsonModule": true, // json 확장자로 import된 모듈을 포함합니다.
    "emitDecoratorMetadata": true, // 소스에 데코레이터 선언에 대한 설계-타입 메타 데이터를 내보냅니다
    "experimentalDecorators": true, // ES 데코레이터에 대한 실험적인 지원을 사용하도록 활성화
    "skipLibCheck": true, // 모든 선언 파일 (*.d.ts)의 타입검사를 건너 뜁니다.
    // "strict": true,
    // "alwaysStrict": true,
    "listEmittedFiles": true, // 컴파일의 일부로 생성된 파일의 이름을 출력 
  },
  "include": [
    "./src/**/*",
    "./bin/**/*",
    "./test/**/*"
  ],
  "typeRoots": [
    "./node_modules/@types"
]
}

 

기본적으로  .env 파일을 사용합니다.  ( 필요한 부분만 작성하였습니다. ) 

PUB_REAL=https://console.ncloud.com
DB_MANAGER_SERVER=http://localhost
DB_CONNECT=false

 

main 함수

const main = async ()=>{
  try{
    /** NSTP Test Runner Param Validation */
    const TestConsole:string = paramValidator();
    /*
     Safety Navigate Console
    */
    await LoginConsole();
    switch(TestConsole.toUpperCase()){
      case 'PUB_BETA':
        await PUBLIC('beta');
        break;
      case 'PUB_REAL':
        await PUBLIC('real');
        break;
      case 'FIN_REAL':
        break;
      case 'FIN_BETA':
        break;
      default:
          Logger.info('Non Selected TestConsole');
        return;
    }
    await Puppeteer.close();
  }
  catch(err){
    Logger.error(err);
    process.exit(1);
  }
}

환경별 실행할수 있는 분기 처리문이 포함된 main 함수입니다.  오늘 포스팅에서는 paramValidator LoginConsole을 알아 보도록 하겠습니다.

 

paramValidator

우선 해당 Test-Runner를 실행하기 위해서는 로컬에서 실행하는 목적이 아닌 Trigger서버를 통해서 실행 될 예정입니다.

Express 서버를 통해 실행 될 예정이고 실행은 아래와 같이 진행됩니다.

app.get('/uuid',async(req,res)=>{
  try {
    const uuid = createUUID();

    res.send(uuid);
  } catch (error) {
    console.error(error);
    res.send(error);
  }
});

app.post('/run',async (req,res)=>{
  const {id,pw,env,platform,uuid}  = req.body;
  console.log(`Test Call ${req.body}`);

  const TaskObject = {
    "account":id,
    "password":pw,
    "env":env,
    "platform":platform,
    "uuid":uuid
  }

  console.log(TaskObject);
  runTestRunnerExec(TaskObject);
  res.send(uuid);
})
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

 // npm start account passwd pub_beta classic uuid

function runTestRunnerExec(TaskObject){
  let command =`npm start ${TaskObject.account} ${TaskObject.password} ${TaskObject.env} ${TaskObject.platform} ${TaskObject.uuid}`;
  exec(command,(err,out,stderr)=>{
    console.log(out);
  });
}
function createUUID() {
  let d = new Date().getTime();
  if (
    typeof performance !== 'undefined' &&
    typeof performance.now === 'function'
  ) {
    d += performance.now(); // use high -precision timer if available
  }

  return 'task-xxxx-xxxx-4xxx-yxxx'.replace(/[xy]/g, function(c) {
    var r = (d + Math.random() * 16) % 16 | 0;
    d = Math.floor(d / 16);
    return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
  })
 }

코드의 일부를 가져왔으며 실행시 파라미터가 아래와 같습니다.

 npm start account passwd env platform uuid

UUID의 Controller로 UUID를 발급을 받고 해당 Worker Job(UUID) 로 실행 요청을 하기 때문에 해당 의 형태를 가지고 있어 Test-Runner에서도 한번 유효성 검사를 하는 구간을 두자는 의미에서 작성하였습니다.

 

function platformValidate(param:string) : void {
  const testPlatForm = {
    "FIN_REAL":process.env.FIN_REAL,
    "FIN_BETA":process.env.FIN_BETA,
    "GOV_REAL":process.env.GOV_REAL,
    "GOV_BETA":process.env.GOV_BETA,
    "PUB_REAL":process.env.PUB_REAL,
    "PUB_BETA":process.env.PUB_BETA
  }
  if(param==="undefined" || param===undefined || param===null){
    Logger.error('Please Check your Runtime Parameter ');
    Logger.error(`1. param is -> ${param}`);
    process.exit(0);
  }

  
  let _param:string = param.toUpperCase();
  const url = testPlatForm[_param];

  if(url==="undefined" || url===undefined || url===null){
    Logger.error(`undefined Get URL : ${url}`);
    Logger.error('Check List');
    Logger.error('1. Platform Param ');
    Logger.error('2. Directory Root Check your .env File ');
    process.exit(0);
  }
  process.env.url=url;
  return url;
}
export const paramValidator = ()=>{
  console.log(process.argv[6])
  if(process.env.NODE_ENV==='development'){
      Logger.info('Development Env');
  }
  else{
    if(process.argv.length <= 5){
      Logger.error('Parameter Validation Error');
      Logger.error('Please set [ID], [PassWord], [TargetPlatForm] ');
      Logger.error(`Current Param ID : ${process.argv[2]}`)    
      Logger.error(`Current Param Password : ${process.argv[3]}`)    
      Logger.error(`Current Param Test Console : ${process.argv[4]}`)  
      throw new Error('Param Validation Error');  
    }
    else if(typeof process.argv[6]==="undefined"){
      Logger.error('UUID is not Exist');
      throw new Error('Required UUID Param');
    }
    let ID = process.argv[2];
    var emailReg = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
    if(!emailReg.test(ID)){
      throw new Error(`ID Email Error is not Email ${ID}`);
    }
    Logger.info(`Automation URL ${platformValidate(process.argv[4])}`);
    
    for(let i=2;i<process.argv.length;i++){
      Logger.info(process.argv[i]);
    }
    // global UUID variable
    process.env.nstpUUID = process.argv[6];
  }
  
  return process.argv[4];
}

간단하게 넘어가자면 5개의 파라미터를 기본으로 받아야하기 때문에 

account - 로그인 할 계정

passwd - 로그인 할 계정의 비밀번호

env  - 환경  ( 네이버 클라우드는 민간, 금융, 공공 3가지의 환경을 가지고 있습니다. ) 

platform - 플랫폼 ( Classic, VPC 의 2가지 플랫폼을 지원 합니다.)

uuid - UUID로 생성되어 발급된 UUID

(* 현재는 해외리전도 있는걸로 보아 해외리전 파라미터도 추가가 되어야겠네요 제대로 작업해서 만든다면요 )

 

1. 파라미터 개수 및 파라미터 확인 

2. account가 email인지 확인 

3. 파라미터에 따른 .env에서 URL을 정상적으로 가져왔는지 확인 

etc. 환경등을 확인하는 조건문 등이 있긴한데 샘플 파일이라고 귀엽게 넘어 가주시면 좋겠습니다. ㅜ

 

오류 화면

 

정상 실행 및 이동&amp;amp;amp;amp;nbsp;

파라미터 유효성 검사가 끝났다면 Console 로그인을 진행 하도록 하겠습니다.

 

LoginConsole

로그인 콘솔입니다. 사실 이렇게까지 해야되나 싶기도하지만 LoginConsole을 로그인 모듈로 빼서 여러 곳에서 호출해서 쓴다고 가정을 하고 작업을 하다보니 꽤 많은 양의 작업이 되었습니다.

 

우선 파라미터로 id,pw를 넘겼기 때문에 process.argv를 통해 파라미터 값을 가져옵니다. ( id,pw의 순서는 고정 ) 

getBrowser를 통해 퍼펫티어의 기본 세팅을 한후 브라우저를 실행합니다.

getBrowser는 API 가 아닙니다.

싱글톤 객체로 사용중인 자체 함수입니다. initialize 는 스킵하도록 하겠습니다. ( 원한다는 요청이 있으면 다시 재포스팅하겠습니다.)

 

그리고 나오는 isLoginPage 일단 해당 설명을 하기전에 NaverCloudPlatform의 페이지이동에 대한 히스토리부터 생각해보겠습니다.

 

보시면 바로 console.ncloud.com 으로 가게 되는데 로그인이 되어있지 않는 상태 의 세션 브라우저를 확인시 순서는 아래와같습니다.

 

1. 로그인창

 

2. 비밀번호 변경 정책에 어긋 났을 경우 

 

 

3. 비밀번호 변경 정책에 어긋나지 않았을 경우 ( 콘솔 페이지로 이동 )

이동하였지만 Dimmed 처리에대한 다시 보지않기 처리가 PC에 세팅되어있지 않은 경우

 

4. Dimmed 처리에 다시보지않기 를 설정한 기록이있어 정상적으로 이동된 경우

 

 

 

여기까지가 로그인 을 하여 콘솔까지의 정상이동으로 볼수 있습니다. 이와중에 해결해야하는 부분은 아래와 같습니다.

 

1. 로그인 정상 처리

2. 로그인 정책 위반시 다음으로 넘길수있는 처리

3. Dimmed 처리 안되어있는 PC의 경우 Dimmed에 대한 처리 

 

로그인 정상 처리

process.argv로 ID, PW를 받아온 상태에서 이제는 입력을 해야하고 입력하기 위한 Element를 찾아야 합니다. 모든 사람들이 Element를 찾는 방법은 수십 수백개라 저는 저만의 방식을 찾은 거를 포스팅하겠습니다.

 

그전에 현재 페이지가 login 페이지가 맞는지 먼저 체크합니다.  ( explicitlyWait도 API 가 아닙니다. 자체 제작한 함수로 일단은 스킵하겠습니다.  해당 함수만 포스팅하려면 한페이지가 작성되어야해서 ;; )

핵심은 selector 부분입니다. 해당 css 셀렉터를 통해서 해당 페이지 유무를 확인합니다.

이미지시에 보이는 셀렉터를 기준으로 잡은 거였습니다.   로그인 페이지는 SPA Web FE 개발자에 잠시 빙의를 해보자면 Login Component를 따로 쓸걸로 예상되기 때문에 해당 페이지는 웬만하면 큰 변화가 없을 것을 기대합니다.

 

그렇다고 Chrome브라우저에 JS CSS 복사 기능을 쓰게 되면 아래와 같이 나타나는데 

document.querySelector("#username")

위처럼 사용해도 될수 있지만 해당 페이지에서 유일하게 placeholder에 아이디* 로 들어가게되는 경우는 input box 뿐이라고 생각되어서 위의 CSS Selector를 사용하였습니다. ( 저는 xpath 는 사용안하고 CSS Selector를 선호합니다.  포스팅하려면 할수있는게 엄청많군요 )

 

 이렇게 현재 페이지가 Login Page라는 점이 확인되면 Inputbox와 Password box를 찾아서 입력 후 로그인 버튼을 선택합니다. ( 이부분은 약식으로 하겠습니다. ) 

 

간단하게 입력 후 저는 Enter 키로 Navigate 를 진행했습니다.

page.waitForNavigation() -> 퍼펫티어의 API 인데 MPA에서는 문제가 되지 않지만 SPA ( Single Page Application ) 에서는 많은 문제가 됩니다. 현재 만들어진 페이지가 Vue, React, Angular 같은 SPA로 만들었는지 확인이 되어야합니다. 아니면 현재 페이지가 서버 라우팅인지, 클라이언트 라우팅인지 명확한 확인이 된 후에 해당 API 써야 합니다. 그렇지 않으면 Timeout이 나게 됩니다. - 이부분도 따로 포스팅을...

 

 

엔터까지 입력이 된다면 이제 해당 사이트의 로그인 정책에 위반되었을 케이스도 확인 하여야 합니다.

로그인 정책 위반시 다음으로 넘길수있는 처리

위에 보시면 이동후에 isDashBoard -> 네 맞습니다. 콘솔 대쉬보드로 이동이 됐는지 체크하는겁니다.

확인내용은 다음과 같습니다. (현재 URL , 이동할 URL) 을 받아와서 비교합니다.

이동하려는 페이지(대쉬보드 URL )가 아니라면 나올수있는 페이지는 비밀번호 정책 페이지 또는 비밀번호 오류 가 있을수 있습니다.

 

1. 대쉬보드 일 경우 ( Successfully )

2. 대쉬보드가 아닐 경우

   2-1 ( passwordAFterwardsCheck ( 정책위반이 될경우 - 비밀번호 변경 ) )

   2-2 ( 그외에 오류 ) - 비밀번호 가 틀리거나 ID를 잘못입력했거나 -> Exception Error

 

 

 

passwordAfterwardsCheck ( 비밀번호 변경 후 90일이 지났을 경우 에 대한 예외처리 )

async function passwordAfterwardsCheck(page:puppeteer.Page,currentURL,navigateURL){
  const selector = '.loginSecure';
  const loginSecureEle = await Puppeteer.explicitlyWait(page,selector);

  /** Check passwordChange Request Page */
  if(loginSecureEle!==false && typeof loginSecureEle !=="boolean"){
    Logger.info(`🚧 The current page is the password change request page. `);
    Logger.info('🚧 Proceed to change the default setting afterwards.');

    let props = await Puppeteer.getProps(page,loginSecureEle,'innerText');
    let regEx = /비밀번호변경 안내|90일마다/gi;
    let changePasswordCheck = regEx.test(props);
    /** Check passwordChange Request Button */
    /** 30일 비밀번호 변경 버튼 유무 확인  */
    if(changePasswordCheck){ 
      Logger.info('[Check] nextChangeButton ');
      const selector = '.loginSecure button';
      const buttons = await Puppeteer.explicitlyWaits(page,selector);

      let nextChangeBtn;
      await Array.prototype.reduce.call(buttons,async(prev,curr)=>{
        const nextItem = await prev;
        let itemText = await Puppeteer.getProps(page,curr,'innerText');
        let diffText = itemText.replace(/\r\n|\n| |\s/gi,"");
        Logger.info(`[diffText] : ${diffText}`);

        if(/다음에변경하기/gi.test(diffText)){
          Logger.info('[Check-Success] nextChangeButton ');
          nextChangeBtn = curr;
        };

        return nextItem;
      },Promise.resolve());
      if(nextChangeBtn==="undefined" || 
      nextChangeBtn===undefined || 
      nextChangeBtn ==="null" || 
      nextChangeBtn===null){ 
        //! nextChange Button is NULL
         Logger.error('[Check-Failed] nextChange Button is not Exist'); 
         return false;
      }
      else{
        Logger.info(`[Click] passwordAfterwards Button`);
        // * Console로 변경 될 때 까지 클릭 
        await Puppeteer.forcedClick(page,nextChangeBtn,"다음에 변경하기",async ()=>{
          try {
            let regExp = /[A-Za-z.-]+/g;
            let currentURL:any = await page?.url();
            let currentProtocolUrl = currentURL.match(regExp)[1];
            let navigateProtocolUrl = navigateURL.match(regExp)[1];
            Logger.info(`[Current] ${currentProtocolUrl}  <----> [Target] ${navigateProtocolUrl}`)

            return currentProtocolUrl===navigateProtocolUrl;
          } catch (error) {
            console.error(error);
            return false;
          }
        });

        //? passwordAfterwardsCheck Success Return  
        return true;
      }
    }

    /** InValid ID or InValid Password */
    else{
      Logger.error('changePasswordCheck is not Exist');
      return false;
    }
  }
  else{
    Logger.info('not Exist loginSecureEle ');
    Logger.info('Login Actions Check');
    const loginRootElement = await Puppeteer.explicitlyWait(page,'.center-wrap.mh-20');
    let resultCheck;
    if(loginRootElement!==false && typeof loginRootElement !=="boolean"){
      resultCheck = await Puppeteer.getProps(page,loginRootElement,'innerHTML');
    }

    if(resultCheck.match(/아이디(메일)를 확인해 주세요/gi) ){
      Logger.error('[Login Actions] ID/Mail Error');
    }
    else if(resultCheck.match(/패스워드 오류/gi) &&
    resultCheck.match(/패스워드 오류 : 0회/gi)){
      Logger.error(('[Login Actions] ID Error'));
    }
    else if(resultCheck.match(/패스워드 오류/gi)){
      Logger.error(('[Login Actions] PassWord Error'));
    }
    else{
      Logger.error('[Login Actions] UnKnown Login Error');
    }
    return false;
  }

}

 

해당의 페이지도 마찬가지로 공통 Component 로 보여지는 페이지 이기때문에 정책변경에 대한 .loginSecure 를 찾습니다. 지금 보니까 article semantic tags를 사용하였기 때문에 article .loginsecure 로 찾는 게 조금 더 유니크 할수 있겠네요

 

forcedClick 같은것도 자체 제작 API 입니다.  해당 코드가 정상적으로 이동되면 이제 콘솔대쉬보드로 이동되면서

Dimmed 처리가 나타나게 됩니다. 

 

Dimmed 처리의 핵심부터 말씀드리자면

 

.coach-mark 입니다.  해당 이 body 역할을 하고 있는 것으로 확인되며 해당 Dimmed 처리 역시 해당 Element Body 를 이용해서 찾은 후  변하지 않는 유니크 값을 찾습니다. 

/환영합니다.|님,|다시 보지 않기/gi;

 

 

async function dimmedCloseActions(page:puppeteer.Page):Promise<boolean>{
  const selector = '.coach-mark';
  const dimmedEle = await Puppeteer.explicitlyWait(page,selector);

  if(dimmedEle!==false && typeof dimmedEle !=="boolean"){

    let diText = await Puppeteer.getProps(page,dimmedEle,"innerText");
    let regex = /환영합니다.|님,|다시 보지 않기/gi;
    await page.waitFor(500);

    if(regex.test(diText)===true){
      Logger.info("🚧 Dimmed is output and Close Window.");
      let dimmedBtnArr = await Puppeteer.explicitlyWaits(dimmedEle,'.btn');
      let dimmedCloseBtn;

      //! dimmedBtnArr 이 undefined 거나 NULL일 경우에 대한 분기 처리 필요 

      await Array.prototype.reduce.call(dimmedBtnArr,async(prev,curr)=>{
        let nextItem = await prev;
        let btnText = await Puppeteer.getProps(dimmedEle,curr,'innerText');  
        let diffText = btnText.replace(/\r\n|\n| |\s/gi,"");
        
        if(/닫기/gi.test(diffText)){
          dimmedCloseBtn = curr;
        }
        return nextItem;
      },Promise.resolve());

        await Puppeteer.forcedClick(page,dimmedCloseBtn,"딤드 처리 제거",async ()=>{
          try{
            let afterDimmedEle = await Puppeteer.explicitlyWait(page,'.coach-mark',3,500);
            if(afterDimmedEle===false){
              return true;
            }
            else{
              return false;
            }
          }
          catch(err){
            console.error(err);
            return false;
          }
        })
      return true;
    }
    else{
      return false;
    }
  }
  else{
    Logger.info('🚧 Dimmed was not output .Process to the next Step .');
    return true;
  }

}

 

저는 여기까지 유니크한 값으로 비밀번호 정책 및 Dimmed처리를 진행하였습니다.

이후에 로그인 및 대쉬보드 이동 테스트를 500번정도 진행하였으나 현재까진 오류가 없었습니다.

 

수정사항이있다면 추가로 관리하겠습니다.

 

 

 

 

 

감사합니다.

 

*** Place Holder  값이 바뀌여서 Login 함수의 수정이 필요하게되었습니다.

 

안녕하세요 깍돌이입니다.  NaverCloudPlatform 을 기준으로 잡고 Puppeteer 를 통해서 UI 자동화 하는 작업들을 포스팅 하려고합니다.

 

UI 자동화에 대해서는 할말이 매우 많지만 그거는 개인생각 끄적끄적에서 다뤄보도록 하겠습니다.

 

Portal -> Console

Portal -> Portal 에 대해서 체크할수 있는 방법 

 

 

레코딩

간혹 레코딩 방식에 대해서 말씀하시는 분들이 계신데 경험상으로 느꼈지만 UI 자동화에 대한 토론을 해본적이 없었기에 현재 세상에서 UI를 레코딩으로 하는것만큼 비효율적인 게 있을까 싶긴합니다.

Pass / Fail 유무는 어떻게 판단할 것인가

=> 이미지로 판단한다고 하면 OpenCv Template Matching을 이용하여서 기대 이미지로 체크할것도 아니고 더 복잡한 케이스가 된다고 생각합니다.

예외처리에 대해서 어떻게 처리할 것인가

 

=> 중간에 예외 처리로 종료 되었을 때 Suspend , Resume에 대한 처리는 어떻게 할것 이며 이부분에 대해서 시스템 오류인지 제품에 대한 오류인지 판단을 어떤식으로 할지 10년 전에야 레코딩 방식이 거의 주를 이루었고 그걸 기준으로 자동화가 이루어졌지만 현재 시대 및 앞으로의 방향성에서는 레코딩 방식은 전혀 좋은 방법이 아니라고 생각 합니다. 이에 대해 다른 견해를 가지신분이 있다면 이야기 하면 너무 좋을거 같네요

 

결과에 따른 신뢰도 바탕이 될 수 있는 것인가

=> 사람이 눈으로 보지 않는 한 레코딩의 Pass / Fail 판단부터 명확하지 않는데 이부분에 대해서 어떻게 할지 마땅이 떠오르지 않습니다.

 

신입 때 자동화 발표를 봤었는데 시나리오는 아래와 같았습니다.

 

페이지 이동 -> 로그인 -> 검색 -> 리스트 

 

레코딩으로 진행된 발표였으며 Pass / Fail 기준은 리스트까지 이동하고 오류없이 진행됐으면 Pass 였습니다.

그래서 질문을 했습니다. 

질문 : " 리스트에 결과가 안나왔어도 성공인가요?

답변 : "네 맞습니다. "

이유는 기준으로 정해놓은 시나리오는 성공했기 때문이라는 답변을 받았습니다.  문제는 없는 답변이였습니다. 리스트페이지까지 이동하는게 시나리오에 제목이였기 때문에 그래서 다시 물어봤던 기억이 납니다.

질문 :  "리스트에 대한 케이스 자동화는 안되는건가요? "

답변 :  "그런건 직접 보고있습니다."

=> 사실 리스트를 클릭하게 하고 Exception 으로 종료시키는 방법도 있지만 레코딩 방법은 한계가 명확한 방법이라고 생각합니다.

 

Element DOM Native Code

그렇다고 Element 기반으로 하게 되었을때 그만큼 좋냐라고 물어보게 된다면 그것도 확실한 대답을 드리려면

"잘해야" 좋다고 할수있을거 같습니다. 그리고 또한 잘하냐 못하냐에 문제가 해결된다고 해도 직접 코드로 처리로 작성되는 경우는 숙련되지않는 한 공수가 매우 많이 들어가게 됩니다.

필자는 잘한다고 생각하진 않지만 꽤 숙련된 사람이라고 생각 합니다.   그러면 여기서 "잘하기가 어렵지 않냐" 라고 하시는 분들이 간혹 있었습니다. 

QA는 제가 생각하기에 개발자가 만든 제품에 대한 "테스터"가 아닙니다. "품질관리자" 입니다. 그렇기 때문에 당연히 개발 경험도 있어야 하고 최소한 시니어 개발자가 아닌 주니어 개발자 만큼 혹은 그 이상의 뷰를 가질수 있어야 한다고 생각합니다. 외국계에서는 시니어 개발자가 QA로 넘어 가서 주니어개발자들의 코드 및 품질을 봐주는 케이스 처럼입니다. 

 

그리고 잘하기 위해서 노력해야 한다고도 생각합니다.  QA로써 Web 자동화를 한다면 최소한의 Browser 가 어떻게 Syntax Tree를 그리고 DOM 에 Element를 붙인후에 CSS를 입혀서 스타일링되고 현재시대에 와서 MPA가 아닌 SPA에서의 웹 자동화 테스트를 위해서 어떤식으로 돌아가는지는 알고 완벽하진 않지만 완벽함을 추구하는 테스트 자동화를 구성해야 한다고 생각 합니다.

앱도 마찬가지입니다. 최소한 IOS / APP 의 View Life Cycle은 알고 있어야 한다고 생각 합니다. 

 

테스트 자동화 케이스는 만드는거 자체는 어렵지 않습니다. 어떻게 보면 쉽다고도 할수 있습니다. UI Automation 어디에서 검색해도 관련 튜토리얼은 너무나 많고 동작 하나하나에 대한 API 도 매우 간단하고 설명도 잘되어있습니다. 

여기서 어려운거는 신뢰도 있는 케이스를 만들어 내는 것이라고 생각 합니다.  지금 실패가 정말 제품에 대한 오류인가에 대한 것에 대해서 

 

그래서 개인적으로는 많이 실행해본 사람이 더 잘할 거라고 생각합니다. Exception 처리에 대한 경험이 곧 실력이 될수도 있다고도 생각합니다.

 

포스팅을 통해서 실행이라도 많이 하여야 현재 필요한 시점에 자유롭게 쓸수 있을거 같아서 작성을 준비합니다.

 

 

 

 

 

 

- 임시 포스팅 입니다. ㅜ

안녕하세요 깍돌이입니다.  테스트 자동화 플랫폼 개발기에서 Detail VIEW와 Image VIEW에 대한 기본 구현이 되었는데요  (아직 포스팅안됨) 해당의 Image VIEW의 경우 이미지를 통해서 하다보니까 프레임이 낮고 그러다보니 아쉬운점이 이만 저만이 아닙니다. 그렇기 때문에 해당 자동화 테스트 화면을 실시간으로 스트리밍을 하는게 궁극적인 목표인데요

 

그렇기 위해서는 아래의 작업들이 필요합니다. 

OBS ( Open Broadcaster Software ) 

방송과 녹화를 할수있는 오픈소스 프로그램으로 해당을 통해서 화면 캡처를 하게 됩니다.

 

RTMP Server

OBS으로 캡처된 화면을 RTMP Server로 보내게 되며 해당 화면들을 받아서 처리합니다.

 

ffmpeg && HLS 인코딩

RTMP Server로 받아온 캡처 내용들을 ffmpeg을 통해서 hls로 인코딩합니다.

저장소 저장

저장된 HLS 파일을 특정 저장소에 저장합니다.( S3, Object Storage ) 

hls url 배포

저장소에 저장된 경우 해당 URL이 있기 때문에 배포가 완료 될 경우 hls url을 배포합니다. 그리고 Client FE에서 배포가 된점을 알수 있게 합니다.

FE Client HLS Player 재생 

FE Client에서는 해당 배포 내용을 받고 스트리밍을 시작합니다.

 

( 작업 내용 추후 포스팅 ) 

임시 포스팅

 

Mysql 에서 시간 저장을 위해서 datetime 으로 저장하게 되는데

해당의 경우 DB로부터 데이터를 받아 그대로 렌더링시

 

이와같이 나타나게 해주기 때문에 이를 위해 여러가지 구글링시

첫번째 dbconfig의 dateStrings를  'date' 로 설정합니다.  

 

 

 

 

 

안녕하세요 깍돌이입니당~ 

 

저희집 가훈 중 하나가 시간은 만드는 것이다 라는게 있는데 그래서 어머니께서는 시간이 없다라는 말을 별로 안좋아하십니다.

 

그럼에도 불구하고 ㅜㅠ 정말 시간이 없네요  막 엄청 부지런한 성격이 아닌가봐요... 해야될게 많지만 하고싶은것도 많아서

 

자꾸 뒤로 미뤄지게 되네요 그러다보니 평소에 작업하던 내용들을 포스팅 하는것도 있지만 중간중간 생각들을 정리 하려고 합니다.   자동화관련된 이야기를 많이 하게 될거 같습니다. 또한 관련에서 경험에 기반된 케이스가 많기 때문에 양해 부탁드립니다.   첫회사는 QA만 120명이 넘는 회사였고 현재는 작지만 많아지고 있는 상태 입니다.

첫번째로 하고 싶은 말은 Expected Result에 속지 마라입니다.

아마 QA에서 자동화을 하게 되는 상황이나 지켜보는 상황들이 존재 합니다. 저 연차 QA 일때 코드 조금 짤줄 알았던 ( 우물안 개구리였죠 ) 저는 맡은 제품에 API 들을 자동화하면 대충 5500건이 넘어가는데 (당시에 1인 QA) 코드 단으로 하는건 그냥 코드단에서 자동화 하면 쉽지 않을까? 하고 타자가 빠른 저는 며칠 걸리지않아 해당 API 를 자동화 했습니다.

 

API의 몸통이 되는 코드가 제품 안에 이름이 있었고 파라미터는 공용되는것들이 많아 공통 파라미터로 받는 Result가 같은 포맷이였기에 

let apiFncName = getGrepAPINameList();
// 공통 파라미터 설정
let commonparam = ''
apiFncName.reduce((prev,curr,index,item))=>{
      // 함수 이름 꺼내서 
      // 실행 ( 공통 파라미터  ) 
      // 결과 이상없을 경우
      
      // test_result.push(curr.name, result, test_result);
},0);

와 같은 형태로 동작이 가능한 구조였기 때문에 적은 코드로 많은 다량의 API 들을 호출할수 있었고 ( JS가 1급 함수여서 가능했던 작업 같습니다. - C였다면 함수 포인터까지 가야되지만 그러면 머리가 많이 아팠을 듯 )

 

여기서 핵심은 짧은 기간내에 5500개의 API 자동화를 한게 아닙니다.  QA로 일한다면 "테스트" 의 목적을 두면 안됩니다. 필요없는 단순 Test Case 100건 보다 평소에 많은 생각과 구조에 대한 연구가 된 하나의 Test Case 가 제품 릴리즈 시에는 빛을 발하게 된다고 생각합니다. 

위의 5500개의 자동화 케이스를 만들고 돌릴 경우 99% 는 Stdout에서 pass로 발생합니다. 1%의 에러도 내가 생각하지 못하였던 예외처리 였습니다.

 

그럼 거의 API 들을 많이 커버했을까? 그렇지 않습니다. 단순 API 에 대한 커버는 이루어졌지만 해당 자동화 케이스는 위에 말했던 100개의 단순 케이스일 뿐입니다. 그렇기 때문에 추후 발생하는 API 이슈들을 많이 잡아내지 못했습니다. 개인적으로 UI 포함 7천개의 케이스를 돌리고 있던 나는 다시한번 돌아보는 계기가 되었으며 철 없던 저 연차의 시간이 그렇게 지나갔습니다. 그리고 자동화 테스트의 결과는 Expected Result 가 pass 와 fail의 결과를 만들게 되는데 expected Result 또한 담당하는 QA가 작성하게 됩니다.  이로 인해서 너무나 주관적인 견해가 들어가게 됩니다. 그러면서 많은 부류들도 같이 발생합니다.

1. 처음에 생각 했던 대로 진행 ( 쉬운 케이스 부터 얼른 얼른 전체 틀 구조 ) 

자동화 케이스는 기본적인거부터 하는 경우가 많기 때문에 정말 호출 및 결과만 보는 케이스를 작성하고 회사에 보고시

회사에서는 더많은 케이스를 돌리는걸 보고싶어하여 고도화 할시간은 갖지 못하고 계속 저 위에 케이스를 넣게 됩니다. 이 경우는 언젠간 터질지 모르는 시한 폭탄이라고 생각합니다. 오류는 잡지 못하는 자동화 가 되는 수순을 밟게 됩니다.

자동화 없는 자동화 

 2. 어려운거 부터 하는 케이스

복잡한 케이스를 놓치는 경우가 많아서 복잡한 케이스를 우선적으로 하고 그다음에 전체 구조를 고도화 하자라는 목표로 작성시 어찌 보면 Test Case 1건 1건은 공을 들였기에 오류를 잡아 낼수있는 케이스가 될수 있습니다. 하지만 이 경우도 문제가 되는 케이스가 있습니다. 너무 TC에 공들인 나머지 자동화 의 구조를 생각 하지 못하였던 경우입니다. 위에 보고할때 그래도 좀 있어 보이게 해야되는데  공들인 TC가 남아있는데  이걸 아무리 말해도 다른 분들이 이거를 알아주는 경우는 많이 못 보았던거 같습니다.

 

1,2번 모두 아주 지극히 많은 케이스 중 몇개일 뿐입니다.  시작하려다가 안하는 케이스도있을 거고 tool 선정 하다보니 안되는 케이스도 있을거고 처음부터 내가 간단하게 개발을 해서 하겠다는 케이스등 여러 케이스가 있지만 공통되는 경우는 테스트의 결과에 대한 pass fail은 작성한 QA 의 expected result 입니다.

 

createServerAPI 

서버를 생성하는 API 입니다. 해당 API 는 생성할때 필요한 생성 값( 파라미터 ) 를 받아서 서버 생성 요청을 넣고 ( VDI ) 방금 request를 보낸 client에게는 바로 서버의 Info( 정보 ) 를 Response로 던집니다.

 

API 를 자동화를 하려고 할때 Expected Result를 이렇게 하는 경우도 있습니다. 해당 API 주소로 HTTP Method를 request를 보낸후에 response를 받고 Pass 로 표시할수 있습니다.

 

이경우는 반쪽짜리 케이스라고 볼수 있습니다. createServerAPI는 FE와 BE간의 요청을 전달하는 역할을 하고 "실제 생성"은 VDI를 통해서 이루어지기 때문입니다. Unit Test 를 하는게 아니기 때문에 QA입장에서는 사용자가 쓸수 있는가 정도 까지 생각을 해봐야 합니다.  API 를 테스트하는 것이 아닌 API 의 품질보는 방향에서는 Expected Result가 확연하게 달라질 필요가 있습니다.

 

1. API Requeset ( 생성 파라미터 ) 

2. API Response 확인 

3. API Request ( 서버 리스트 혹은 상태를 가져오는 API 확인 ) 

4. 생성이 되지않았을 경우 Polling 하여 확인 

5. 생성이 되었다면 Pass로 종료 생성이 되지않았다면 QA가 지정한 Condition Timeout까지 Polling 해당 시간초과시 내부적으로 해당 오류와 함께 오류 리포트

 

업무를 하다보면 API 테스트에 대해서 간단하게 생각 하시는 사람들이 있습니다. 직군과 사람들 간의 차이로 인해 발생하는 것으로 "테스트"의 관점을 두고 생각 하냐 "품질"의 관점을 두고 생각 하냐에 따른 차이인거 같습니다.

 

테스트의 관점을 둔다면 API 자동화 테스트는 소위 말하는 "노다지" 입니다.  작성하기도 쉽고 그걸 보여주기도 쉽고 테스트 시간도 적절하고 하지만 품질 관점에서의 API 자동화 테스트를 추가로 한다면 (본업이 있기에) 기존 Test Case를 가지고 Test Plan 을 짜듯이 사용자 입장에서 정상이라고 느껴지는 지점과 그에 대한 Expected Result에 대해 많이 고민해야 합니다.

 

내가 아닌 누군가가 자동화를 했다고 할 경우 리뷰를 해야하는 경우가있다면 잘돌아가나만 보는게 아니라 Expected Result를 무엇으로 기준을 했냐가 중요합니다.  현재 상황에 따라서 위와같이 못한다고 해도 중요하다고 하는 부분의 경우 Expected Result를 다시 한번 다듬어 볼 필요는 있는거 같습니다.

 

이부분에 대해서 기술 포스팅은 추후에 꼭 하도록 하겠습니다.

 

감사합니다.

 

 

 

 

 

 

허니포레스트
산속 밭들 사이에 있기때문에 밤에 갈 경우 도착직전까지 잘못왔나 착각 할 수 있음
커피 맛은 괜찮

밖에 뷰

+ Recent posts