안녕하세요 깍돌이 입니다. Timer Phase 에서 Worker ( 테스트 자동화 코드 ) 를 실행하면서

 

느꼈던 문제들 그리고 Node.js의 Event Loop 에 있는 Timer Phase 에 대한 생각 그리고 위의 Worker들을

 

Worker Thread Pool 로 해결이 될지 된다면 어떤방법인지에 대해 생각한 내용들입니다.

 

Node.js Timer Phase 

네 타이머 페이즈에서 해결할수 있었습니다. Event Loop 가 계속 돌면서 타이머 페이즈에 는 계속 등록된 함수들이 있게 되고 ( Worker ) Event Loop 가 돌면서 계속 실행됩니다.  하지만 테스트 플랫폼의 초기 단계이기도 하고 API 테스트의 구조는 비동기 형태로 단순 Request , Response 에 대한 결과를 검증 하기 때문에 큰 문제는 되지않았지만 ( 이것도 케이스가 수천건이 넘어가면 이슈가 생길듯 ) 

 

시나리오의 경우 UI, API 가 있고 현재는 API 를 우선하고있지만서도 스텝과 스텝 사이의 Polling 하는 구조가 많이 있다보니 Event loop 에서 Timer Phase가 점점 무거워집니다. 그렇기 때문에 결국 Timer Phase 도 뭔가 좋은 방법은 아니지 않을까? 라는 생각을 하게 되었습니다.  ( 이번 작업은 UI 를 하지 않았지만 UI 의 경우 Child_Process를 통하여 직접적으로 하나의 프로세스를 더 띄우는 방법을 생각 하고 있었습니다. ) 

 

그리고 테스트 해보고 싶었던 마음에 노드 관련 홈페이지를 기웃 기웃하다 보니 Node.js 공식 홈페이지에 아래와같은 말이 있었습니다. 

 

그리고 앞선 Worker Pool 에 대한 이야기 중 간과해야 될게 있습니다. 

 

현재 작성하고있는 테스트 플랫폼의 Test Case들은 CPU I/O 가 많지 않습니다.(그렇기 때문에 무조건 워커 스레드 풀을 써야하는게 아닌 Timer Phase 에서 처리되는 워커들이 워커 스레드 풀에선 좀 더 나을까?라는 내용에 대한 확인 을 하고 싶기 때문입니다. )   케이스 스텝 사이에 대기(Polling) 하는 텀이 길기 때문에 하나의 Task(작업) 이 일어날때 무거운 작업은 아니라는 점을 중간에 한번 적어보았습니다.  TC의 예시

 

A시나리오

 

               -> A작업이 완료가 됐는지 최대 10분간 확인 ( Polling) -> 10분이 지나도 완료가 안됐을 경우 -> 실패

A 작업 

            ->  10초마다 확인하여 10분(Test Timeout) 전에 작업이 완료되면 다시 다음 B작업 실행 

 

=> 위와같은 형태로 A -> B -> C -> D -> E -> F -> G -> H -> I -> J - > K -> L (실제 케이스는 이거보다 더많습니다. ) 

로 진행됩니다. 하나의 시나리오에 테스트 시간은 약 30분 정도가 됩니다.  Polling하는 시간이 많기 때문에 CPU I/O를 많이 쓰지 않는 케이스에 대한 자동화라는 점을 기억해 주시기 바랍니다.

Don't Block the Event Loop (or the Worker Pool)

https://nodejs.org/en/docs/guides/dont-block-the-event-loop/

이부분에서 핵심내용으로 생각되는 부분들에 대해서 간략하게 몇가지 적어 보자면  

    Node.js는 EventLoop ( Initialization and callbacks ) 에서  JavaScript 코드를 실행하고 파일 I/O 같은 값비싼 작업을 처리하기 위해서 작업자 풀 ( Worker Thread Pool ) 을 제공한다고 합니다.  

 

두가지 타입의 쓰레드가 존재 

첫번째는 Event Loop ( Main Loop, Main Thread , event Thread ) - 전 포스팅에 있던 노드 이벤트 루프

두번째는 Worker Pool ( Thread Pool ) 

Node.js의 경우 Event-Driven 구조이며 관리를 위한 Event Loop 와 비싼 작업을 하는 Worker Pool 이 있다.

 

그 밑에는 O(n) 의 복잡도를 가지는 콜백에 대해서 작은 n이라면 무엇보다 빠르지만 점점 높아질수록 문제가 생김에 대한 내용이 있고 Node.js는 V8 엔진을 사용하기 때문에 일반적인 조작은 매우고속이며 JSON 연산과 regexps에서는 예외가 될수 있음을 설명 합니다.  regexp는 입력 스트링에 따라서 o(2n) 이 가능하기 때문에 이 경우는 Event Loop를 차단합니다. 

그래서 regexp를 이용한 redos에 대한 샘플이 있지만 ( 테스트 자동화에서는 정규식을 많이 사용하지도 않으며.. 내부 백오피스 에서 사용하기 때문에 일단 제외 해도 될거 같습니다. ) 

결론은 regexp 를 조심해서 써야 한다 이런내용

 

Node.js의 Event Loop를 차단하는 코어 모듈 

Encryption

Compression

File System

Child Process ( 이건 추후에 쓸 예정인데 유심히 봐야될듯 ) 

 

위의 API 들은 상당한 계산을 잠재적으로 사용하기 때문에 많은 비용이 발생 하기 때문에 서버를 구성한다면 아래의 API 들을 사용하면 안된다. ( express를 예로 들면 express Router에서 쓰지 말라는 의미입니다. ) 

  • Encryption:
    • crypto.randomBytes (synchronous version)
    • crypto.randomFillSync
    • crypto.pbkdf2Sync
    • You should also be careful about providing large input to the encryption and decryption routines.
  • Compression:
    • zlib.inflateSync
    • zlib.deflateSync
  • File system:
    • Do not use the synchronous file system APIs. For example, if the file you access is in a distributed file system like NFS, access times can vary widely.
  • Child process:
    • child_process.spawnSync
    • child_process.execSync
    • child_process.execFileSync

API 자동화에서는 CryptoJS를 사용하는데 확인해봐야 될거 같습니다.  파일시스템의 NFS는 NAS같은 류를 이야기 하는거 같습니다.  대부분 동기적인 작업 및 CPU I/O가 순간적으로 많이 발생되는 API 들이라고 생각 됩니다.

 

해당 밑에는JSON DOS ( JSON 에 대한 이야기니 넘어 가겠습니다. ) 

Complex calculations without blocking the Event Loop

이제 뭔가 나왔습니다. 이벤트 루프를 막지 않고 복잡한 계산 에 대해서 파티셔닝과 오프로드 2가지 에 대해서 설명합니다.  이부분은 간략하게 적고 직접 테스트하면서 결과를 적어 보겠습니다.

 

첫번째

C++ 애드온을 개발하여 내장된 Node.js Worker Pool 을 직접 쓴다가 있습니다. 

node-webworekr-thread 를 이용하여 Node.js Worker Pool 에 액세스 하는 js를 사용합니다.

 

두번째

직접 Node.js I/O 테마 워커 풀 대신 계산 전용 워커 풀을 직접 만드는 방법 이다. 방법으로는 Child_Process or Cluster 를 이용한다. 

 

사실 Worker Thread 에 대한 내용을 읽어 보면 CPU I/O 에 대한 이야기가 많다.  왜냐면 해당 작업이 무거워지면 Event Loop 를 막는 경우가 생길거고 이로 인해서 Node.js 전체가 느려지는 현상이 발생하기 때문이다.  서버를 이용한다면 대용량 트래픽을 받을 경우 문제가 생길수 있다 그렇기 때문에 이와같은 형태의 설명들이 많았고 아마 지금 글을 쓰고 있는 저처럼 모든 시나리오의 스텝의 사이가 CPU I/O 가 아닌 대기(Polling) 로 이루어진 케이스가 있을 거라곤 생각을 못했을 것이다.

 

지금 포스팅을 하면서도 트리거 시스템을 이렇게 구성하는게 과연 맞는 것인가? 에 대한 의문이 들지만 나와 유사한 작업을 한 사람도 볼수 없었고 블로그 포스팅도 없었기 때문에 그냥 부딪혀 보면서 포스팅 하고 운영해보다가 더 좋은 개선될 내용이 있다면 다시 포스팅해서 Node.js로 완벽한 트리거 시스템을 구현해 보고자 한다.

 

서론이 쓸데 없이 길었던거 같긴한데 포스팅을 하게되면 이해없이는 쓸수 없기 때문에 서론을 쓰게되면 많은 이해가 된 상태로 작업을 할수 있게 됩니다.

 

해당 테스트는 많으면 2포스팅 적으면 1포스팅으로 하려고 하며 트리거 시스템의 기본 베이스 서버는 Express.js를 쓰기 때문에 Express.js를 가지고 작성하며 Express.js의 기본적인 내용은 다 알고 있다고 생각하고 자잘한 설명은 제외하겠습니다. ( Express.js는 블로그 검색하면 내용이 워낙 많으니까요 ) 

 

https://www.nearform.com/blog/learning-to-swim-with-piscina-the-node-js-worker-pool/

 

 

Express.js

const express = require('express')
const app = express()
const port = 8880

const jsWorker = require('./express_worker');

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.get('/latency',(req,res)=>{

  let st_time = performance.now();
  let ed_time = performance.now();

  let total_time = ed_time-st_time;
  res.send(`latency Time - ${total_time}`);
})

app.get('/express-polling-worker',(req,res)=>{

  let uuid = createStepUUID();
  setTimeout(async()=>{
    try {
      jsWorker(uuid);
    } catch (error) {
      console.error(error);
    }
  },0);
  res.send('Start Polling Worker');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})


function createStepUUID() {
  let d = new Date().getTime();
  if (
    typeof performance !== 'undefined' &&
    typeof performance.now === 'function'
  ) {
    d += performance.now(); // use high -precision timer if available
  }
  return '깍돌이-exps-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);
  })
 }

 

Express_Worker.js

const { setTimeout } = require('timers/promises')

module.exports = (uuid)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      console.warn(`Start UUID ${uuid}`);
      await setTimeout(1000000);
      console.warn(`Close UUID ${uuid}`);
      resolve(true);

    } catch (error) {
      reject('ERROR');
    }
  })
}

 

기본 베이스 코드입니다.

latency 는 해당 컨트롤러에서 코드를 읽고 res.send까지의 시간을 던져 줍니다.

express-polling-worker 는 Timer Phase 에 Timer 를 계속 추가합니다. 위의 코드 베이스로 테스트하겠습니다. 

시간초는 해당 컨트롤러에서 작업이 없기 때문에 보기 편하게 

 

let total_time = ((ed_time-st_time)*1000).toFixed(4);

으로 살짝 보정을 하겠습니다.  ( 사실 express 내에서 하는건 의미가없을 수도 있습니다. node.js의 블록킹이 걸리면 아래 GIF와 같이 코드가 실행되는시간이 오래 걸리는게 아니라 그냥 대기 ( 로딩 ) 시간이 생기기 때문입니다. )

express 내에서 찍어본건 요청이 왔을때 서버 내에서 시간을 한번 체크해보려고 했습니다.

 

Timer Phase 1만개 케이스 Polling

눈치 채셨을 수도 있지만 순식간에 1만개의 함수를 Timer Phase 에 넣었지만 넣고 있던 순간에 잠깐 느려질뿐 넣고 나서는 크게 느리지 않는걸 볼수 있습니다.  해당 이유는 워커에서 실제로 폴링을 한다면  상태 체크 -> 다음함수 또는 다시 상태 체크 의 경우를 해야되는데 하고있지 않기 때문입니다. 해당 코드를 추가하겠습니다.

 

module.exports = (uuid)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      console.warn(`Start UUID ${uuid}`);

      let retry = 0 ;
      let loopCondition = 240;

      // 2400초 = 40분
      
      let closeCondition = true;
      while(loopCondition>=retry && closeCondition){

        let status = await getStatus(loopCondition);

        console.warn(`테스트 상태 체크 ${uuid}  = ${status}`);
        // true 면 반복문 종료 
        if(status==='Y'){
          closeCondition = false;
        }
        await setTimeout(5000);
        retry++;
      }

      console.warn(`Close UUID ${uuid}`);

      resolve(true);
    } catch (error) {
      reject('ERROR');
    }
  })
}

// 30분에 테스트 종료 
const getStatus = (loopCondition)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      // 상태를 가져옵니다.
      let retrunValue='N';

      if(loopCondition===180){
        retrunValue='Y';
      }
      resolve(retrunValue);
    } catch (error) {
      reject(error);      
    }
  })
}

 

Timer Phase 1만개 케이스 Polling

서버에서 계속 동작을 하는 케이스로 추가하였습니다. 워커가 돌기 전 

 

폴링하면서 상태를 체크하는 코드에서 CPU가 과도하게 올라갑니다.

 

1만개의 워커를 사용시 최대 CPU 47%까지 사용하며 레이턴시 ( 서버내에서 실행할 경우 ) 는 큰 변화가 없었습니다.

아마 측정을 FE단에서 요청하고 돌려받는 시간을 재야 할거 같네요 마지막으로 위에 코드에 수정을 하나 하겠습니다.

 

서버쪽과 클라 쪽 입니다.

 

일반적으로는 FE에서는 9의 차이가 나타나지만

 

 

스레드 실행 후 1734 값이 나오는걸 알수 있습니다. 

=> 순간적으로 Timer Phase 에 꽉차기 때문에 병목 현상이 발생한다는 뜻입니다.   ( Event Loop 가 많은 양의 Fn을 Timer Phase 에 등록 하면서 생기는 현상 ) 

1. 케이스가 추가 될경우 병목 현상이 발생할수 있음

2. 추가된 케이스가 계속 수행되면서 CPU 사용량이 매우 높아짐 

 

이제 여기서 Node.js에서 말하는 Child_Process와 Cluster를 이용하여 처리해보려고합니다. 가장 유명한건 Cluster모듈이고 ( pm2 에서 많이 쓰고잇죠 )  해당 Cluster 모듈로 구현된 Node.js Piscina 라는 모듈을 사용해보려고 합니다.

 

Node.js with Piscina (feat. By James Snell

우선 관련 링크입니다.

https://www.nearform.com/blog/learning-to-swim-with-piscina-the-node-js-worker-pool/

해당 링크의 작성자를 유심히 봐야 하는데요 By James Snell 

 

https://github.com/piscinajs/piscina

해당 레포지토리의 메인 컨테이너가 쓴글인걸 알수 있습니다. 그리고 프로젝트들 보시면 알겠지만 Node.js의 개발자이기도 합니다.    해당 포스팅을 이용하여서 위의 테스트한 케이스들에 대해서 Piscina를 적용해보고 결과를 적어 보겠습니다.

해당 글이 2020년도 글이라 현재 많이 바뀐 점이 있습니다. 

runTask X  => run

 

단순하게 바로 테스트 해보겠습니다.

 

const piscina = new Piscina({
  filename: path.resolve(__dirname, 'express_worker')
});


app.get('/express-piscina-worker',async (req,res)=>{


  let loop=10000;
  for(let i=0;i<loop;i++){
      let uuid = createStepUUID();
      console.warn('ppap test - ' + i);
      const result = piscina.run(uuid+'-PPAP');
  }


  res.send('Piscina Worker Start ');
})

Timer Phase 1만개 케이스 Polling ( Node.js with Piscina )

위의 GIF ( without piscina ) 의 비교를 확실히 해보세요 !  같은 코드의 1만건의 테스트 런이 발생했을 때 실제 나의 PC의 CPU 와 웹 FE에서 latency 가 어떤지 비교하면 좋을거 같습니다.

 

1. 1만 TestCase  JS Timer Phase  Worker 실행 

1. 웹화면 지연

setTimer를 이용한 Timer Phase 에 TestCase Worker 를 1만개를 밀어 넣었을 경우 웹화면서 1862 ms 의 지연이 생깁니다. 

2. CPU 사용 률

케이스들의 폴링하면서 상태를 체크할 경우 최대 31% 까지 발생합니다.

* 참고로 제가 사용하는 cpu 입니다.

AMD Ryzen 7 5800X 8-Core Processor 3.80 GHz

 

 

 

1. 1만 TestCase  JS Worker Thread Pool 실행

 

1. 웹화면 지연

Worker Thread Pool 에 1만개의 TestCase를 밀어 넣었을 경우 800ms 가 나옵니다. 

Event Loop을 Blocking 하던 Timer Phase에서 2배의 효율이 발생합니다.

 

2. CPU 사용 률 

보시면 12%를 절대 넘지 않습니다.  

 

뭔가 결과만 보면 Piscina 에서 Worker Thread Pool 을 사용하면서 지연도 2배의 효율이고 CPU 도 오버해서 쓰지 않는거 같습니다.

 

하지만 자세히보면 다른점이 있습니다.  CPU 사용률 체크시 보면 Piscina의 경우 

 

24개의 워커만 실행되는걸 알수 있습니다.  1만대를 실행하였지만 실제로는 워커의 개수만큼인 24개의 워커만 실행되면서 진행한다는 점입니다. Piscina의 구조를 보면 알수 있습니다.

아마 Worker Pool 을 PC의 코어 수 * 1.5 로 만들어 놓고 실행합니다.

 

 

JS timerPhase로 밀어 넣는 경우는 서버의 상태등 아무것도 고려하지않고 밀어 넣기 때문에

 

10000개의 케이스를 전부 Timer Phase에 밀어 넣고 Event Loop 가 돌다가 다시 Timer Phase에 왔을때 실행이 되기 때문에 모든 케이스가 다 실행되며 CPU의 과부하 까지 걸리게 됩니다.

 

그럼 단순하게 여기서 피시나는 24개 뿐 ( Worker Thread )  -> 이것도 제 PC의 CPU 가 16코어이기 때문에 *1.5하여 24개의 스레드가 돌게 되는 겁니다.

 

만약에 스레드가 1초마다 실행하고 종료되는 케이스라면 적절하게 모든 케이스가 추가될건지 확인해보면

 

1만개의 TestCase가 추가된 후 적절하게 테스트 종료 및 대기하던 TestCase 가 다시 시작이 되는 걸 볼수 있습니다. 그런데 이렇게 24개의 워커만 사용해도 10%이상을 쓰지 않는데 좀더 많이 사용할수 없을까? 라는 고민으로 다음 포스팅은 위에서 piscina로 24개 Worker만 사용하는 PC 상태에서를 Core * 1.5배 만큼의 워커를 쓰지 않고 더 쓰는 방법이 있는지와 Piscina의 옵션 하나하나를 설명 하는 포스팅을 하도록 하겠습니다.

 

 

 

 

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

 

해당 포스팅은 사실 QA가 릴리즈 또는 배포 모니터링시에 사용하게되는 툴 중에 자동화 스크립트가 있을수 있습니다.

하지만 해당 포스팅 시리즈는 테스트 플랫폼 ( Test Platform ) 에 관련된 이야기기 때문에

 

이번에 작성되는 내용은 worker 에 대한 내용을 적으려고합니다. 저는 QA하면서 필요한 기술적인 부분들은

전부 Node.js 하나로 처리하고 있습니다. 이부분에 대해서 간략하게 적고 넘어가자면 

Node.js 를 계속 써오고있어서 이기도 하지만 노드가 무조건 짱이다 이런 느낌도 아닙니다.

저연차 QA시절 (지금도 고연차는 아니지만) 에는 메인이 C++이였기 때문입니다.  ( UI 자동화를 IE , Chrome C++ Object 로 하던 시절.. 크흠 )

 

Node.js 하나로 필요한 부분들을 다 커버할수 있기도 하고 에 Node 위주로 사용하고 있으며 효율적으로 사용해야 하기 때문입니다.  ( 하지만 자바로 시작했어도 파이썬이나 노드로 넘어 왔을거 같긴합니다. ) 

 

그리고 궁극적인 목적은 높은 품질의 제품을 제공하기 위함입니다.  높은 품질의 S/W를 제공하기 위해서 자동화 테스트 를 넘어서 테스트 플랫폼이 필요하다면 만들면 됩니다. ( 자동화의 제일 중요한점은 이게 메인이 되선 안되고 QA활동에서 사용되는 Tool 로 써 작용되어야 하며 명확한 이유를 가지고 있어야 합니다. - 이건 대외비라 포스팅하지않습니다. )

 

JS Worker

JS Function 으로 작성된 함수에 대한 Worker 입니다. 

해당 Worker.js의 경우 5초 25초 50초를 대기 후 정상 종료 합니다. 

* 여기서부터 말하는 Worker는 실무에서는 작성될 독립된 자동화 테스트 코드 입니다. 

=> 제가 사용할 자동화 테스트 코드는 폴링하는 경우가 많기 때문에 시나리오 사이사이에 폴링을 유사하게 기대하는 loop 함수를 사용합니다.

 

실행시

순차적으로 실행이 되었습니다.  좀 더 자세히 보기 위해서 로그를 추가 하였습니다.

일반적으로 JS 함수를 통해서 Worker 를 진행시 

다같이 잘 돌아가는거 같지만 모든 함수들이 순차적으로 실행되고 있습니다.

모두 실행전에 결과를 주지않는 함수를 하나 만들어 놓겠습니다.

시나리오는 아래와 같습니다.

하나의 시나리오는 5초 15초 25초의 각각 의 폴링을 가지고 정상 종료 합니다.

2개의 시나리오를 실행시키며 1번째 시나리오에서는 5초 후 return 을 주지 않게됩니다.

 

이렇게만 보면 정상적으로 잘 동작하는 것 처럼 보입니다.

하지만 1에 suspend에서 어떠한 작업도 하지 않았기 때문입니다.  만약에 1에서 suspend 가 true일경우 에러가 발생했다면?

앞에 Worker 에서 발생한 서스펜드 에러로 인해서 뒤에서 실행되어야 모든 워커들이 실행되지 않는 현상이 발생합니다.

=> 해당 이유는 Node.js의 Event Loop 가 Single Thread 이기 때문인것 과 동시에 현재 실행되는 코드는 같은 Phase 에서 실행이 되고 있기 때문입니다. ( 이부분은 점차 수정하면서 설명 하도록 하겠습니다. ) 

 

이 뜻은 결국 각각의 Worker 들이 독립적으로 실행되지 않았다는 반증이 되기도 합니다. 

Node.js의 기본적인 Event Loop 를 같이 사용하다보니 위와같은 이슈가 발생합니다.

 

테스트 플랫폼을 이용하는 QA가 자기자신의 코드가 아닌 다른곳에서의 에러도 신경 써야 한다? 

=> 말이 안되는 이야기 입니다.

 

위의 경우로 인해서 일반적인 JS Function을 사용하는건 말이 안되는 상태입니다. 

Event Loop (Timer Phase)

Node.js의 Event Loop 에는 Timer Phase가 있습니다.

* 출처(Node.js 공식 홈페이지) : https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ 

 

The Node.js Event Loop, Timers, and process.nextTick() | Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

필자의 자동화 시스템에서 Trigger 가 되는 주체는 Express.js로 구현된 서버가 되며 해당 Http 기반의 서버들은 기본적으로 Callback 이 등록되어있기 때문에 지속적으로 Event Loop 가 생성 하고 사라지지 않는 형태가 됩니다 .

그렇기 때문에 서버의 테스트 요청이 왔을 경우 ( 자동화 테스트 코드의 실행 요청 ) 해당 코드를 타이머로 묶어서 타이머 페이즈에 넣어 놓는 방법을 생각해 보았습니다.  

그 결과 코드는 아래와같이 변경 됩니다.   ( Express는 제외하고 샘플 코드를 작성하였습니다. - 실행할 자동화 테스트 코드를 타이머 페이즈로 밀어 넣습니다.)

 

 

위의 JS Worker 에서 오류가 났던 코드 그대로 사용시

뭔가 독립적으로 실행되는것처럼 보이게됩니다.

 

이상태에서 위의 JS Worker 를다시 보면 이거 try catch 를 잘써서 그런거 아닌가? 라는 의문이 다시 들수 있습니다.

setTimeout을 제거하고 Timer phase 로 들어가지않게 코드를 수정하게 되면

2개의 워커가 일단 실행은 되고 비동기 처럼? 돌아가는 것처럼 보이지만 같은 Stack 에서 돌아가고 있기 때문에

결국은 위와같은 현상이 발생하게 됩니다. 

 

이렇게만 보이면 독립적으로 작성된 자동화 테스트 코드를 Timer Phase 에 넣어서 돌아 가고 있는 것 같고 실제로  10개의 worker 를 작성하였을 경우 아래와 같이 (시뮬레이션이지만) 뭔가 잘 돌아가는거 같습니다.

 

하지만 여기에서는 문제가 하나있습니다.  이건 다음 포스팅에 작성 하도록 하겠습니다.

 

다음 포스팅 - 현재의 Timer Phase 에 Worker를 등록해서 사용할 경우 문제점

마지막 포스팅 - 해당 문제를 해결하기 위한 Node.js Piscina

 

안녕하세요 Selenium - Web Driver ( Node.js Version ) 에서 SendKeys 에 대해서 간단하게 알아 보려고 합니다.

 

처음에 SendKeys 에 대해서 사용하게 되는 경우 대부분  자동화 도중에 어떠한 값을 "입력" 하게 되는 경우에

 

사용하게 되면서 시작 하게 될 것 같습니다.

 

// DOM select targetElement
let targetElement = driver.findElement(By.css('div .target'));

// targetElement input String 
await targetElement.sendKeys('Automation Input');

하지만 위와같은 방식 말고 다른 방법으로도 사용이 가능합니다.

 

Automation Copy and Paste with SendKeys 

// DOM select targetElement
let targetElement = driver.findElement(By.css('div .target'));

// targetElement input String 
await targetElement.sendKeys('Automation Input');

// Copy and Paste One Take
await targetElement.sendKeys(Key.CONTROL,'a'); // All Select String
await targetElement.sendKeys(Key.CONTROL,'c'); // Block String to Copy 
await targetElement.sendKeys(Key.CONTROL,'v'); // Paste

뭔가 해당 사항만 봤을 때는  'Automation Input' 이 입력되면 Automation Input 이 들어가고

앞에 Control 값이 있을때 해당 값과 키 값을 같이 누르는 것처럼 보입니다.

 

하지만 실제로는 어떻게 동작하는지 알면 왜 위와 같이 동작하는지 이해하기 편합니다.

 

위의 예시코드는 WebElement.sendKeys 였지만 실제로는 Actions 객체로도 가능한데요

이번 포스팅에서는 WebElement.SendKeys 지만 Actions의 SendKeys 도 비슷한 부분들이 많아

우선 Actions의 SendKeys  부터 간단하게 열어 보았습니다.

 

// Selenium github 

https://github.com/SeleniumHQ/selenium/blob/master/javascript/node/selenium-webdriver/lib/input.js#L572

Selenium 은 오픈소스 이다 보니까 코드 가 공개 되어있는데요

 

SendKeys 의 구현체를 발췌 해 보았습니다.

sendKeys(...keys) {
    const actions = [];
    for (const key of keys) {
      if (typeof key === 'string') {
        for (const symbol of key) {
          actions.push(
              this.keyboard_.keyDown(symbol),
              this.keyboard_.keyUp(symbol));
        }
      } else {
        actions.push(
            this.keyboard_.keyDown(key),
            this.keyboard_.keyUp(key));
      }
    }
    return this.insert(this.keyboard_, ...actions);
  }

1. sendKeys(...keys) -> ...keys 로 spread 연산자 입니다. 들어오는 모든 키들을 파라미터로 받습니다.

   이부분이 이해가 안될수 있는게 

function sendKeys(keys){
	console.log(keys);  // automation
}
sendKeys('automation');

이와같은 형태 아닌가? 라고 잠깐 착각 할수 있습니다. 하지만 Spread Operator 를 사용하게되면 조금 이야기가 달라 질 수 있습니다.

function sendKeys(...keys){
	console.log(keys); // ['automation']
}
sendKeys('automation');

위와의 차이점은 배열(Array) 로 들어온다는 점입니다. 이 뜻은  

A, automation 과같이 입력이 된다는 뜻이 됩니다.

function sendKeys(...keys){
	console.log(keys); // ['CONTROL_A','automation']
}
sendKeys('CONTROL_A','automation');

미리 정의해둔 컨트롤(Control) 이나 쉬프트 (Shift) 값이 들어왔을때 조합키 형태로 사용할 수 있도록 

 

여기에서 잠깐 보고 가야 할 부분이 위에 Key.CONTROL 은 lib/input 에 선언이 되어있기 때문에 사용하기 위해서

 

코드의 최상단에

const { Key } = require('selenium-webdriver/lib/input');

로 가져오신 후 사용하시면 됩니다.

/lib/input 선언부

위와같이 되어있기 때문에 사용하고 싶은 부분들을 사용하시면 됩니다.

 

이제 SendKeys 에서 입력 키들을 배열로 받을수 있도록 처리가 되어 있습니다. 

 

간단하게 CONTROL 만 선언해서 돌려 본다면

 

var Key = {
	CONTROL : '\uE009'
}
function sendKeys(...keys){
   const actions = [];
	 for(const key of keys){
		if(typeof key==='string'){
	     	for(const symbol of key){
				console.log(`symbol ${symbol}`);
            }
        }
		else{
		   console.log(`not Symbol ${key}`);
        }
    }
}

sendKeys(Key.CONTROL,'automation');

테스트 결과

보시면 CONTROL 부분은 심볼로 처리되고 그 외에 있는 문자열들은 따로따로 나눠서 되어있습니다.

미리 선언해놓은 심볼들은 내부적으로 다시 연결하여서 그에 맞는 조합키를 입력 합니다. (해당 내용은 이번 포스팅에서는 제외하고 하겠습니다.) 

 

그리고 해당 값들을 통해서 KeyDown 과 KeyUp을 사용하여 진행합니다.

 

"Automation" 이 파라미터로 넘어간다면

 

A Key Down 

A Key Up

u Key Down

u Key Up 

.... 이 순서로 진행이 된다고 볼수 있습니다.

 

그래서 테스트 해 보았습니다. 

 // 첫번째 요소에 Shift 키와 함께 
 await targetElement.sendKeys(Key.SHIFT,'automation');
 // Result : AUTOMATION
 
 // 두번째 요소에 소문자 입력시 
 await subtargetElement.sendKeys('automation');
 // Result : automation
 

위처럼 현재 Element 에 조합키 + 소문자 입력후 다른 Element 에 automation 만 입력시 어떻게 될지 확인해보았을 시

 

입력중인 위젯에 대해서는 Shift키 심볼로 인해 keyDOWN 상태가 유지되어 대문자로 나오지만

 

새롭게 지정된 요소에서는 해당 키 내용이 없기 때문에 소문자가 그대로 나타납니다.

 this.sequences_ = new Map([
      [this.keyboard_, []],
      [this.mouse_, []],
    ]);
	
    insert(device, ...actions) {
    this.sequence_(device).push(...actions);
    return this.sync_ ? this.synchronize() : this;
  }

위의 sendKeys 마지막 부분 요소에 보면 insert하는 부분이 있게 되는데요 위의 코드는 Selenium 자체코드입니다.

 

Actions.SendKeys 의 경우 관련된 액션과 키 값들을 정리 후 한꺼번에 리턴합니다.  

 

Actions의 경우 perform 이 호출될시 리턴된 액션들을 순차적으로 실행하는 형태이기 때문에 그때 순차적으로 실행됩니다.

 

WebElement의 SendKeys 의 경우

 

async sendKeys(...args) {
    let keys = [];
    (await Promise.all(args)).forEach(key => {
      let type = typeof key;
      if (type === 'number') {
        key = String(key);
      } else if (type !== 'string') {
        throw TypeError('each key must be a number of string; got ' + type);
      }

      // The W3C protocol requires keys to be specified as an array where
      // each element is a single key.
      keys.push(...key.split(''));
    });

    if (!this.driver_.fileDetector_) {
      return this.execute_(
          new command.Command(command.Name.SEND_KEYS_TO_ELEMENT)
              .setParameter('text', keys.join(''))
              .setParameter('value', keys));
    }

    keys =
        await this.driver_.fileDetector_.handleFile(
            this.driver_, keys.join(''));
    return this.execute_(
        new command.Command(command.Name.SEND_KEYS_TO_ELEMENT)
            .setParameter('text', keys)
            .setParameter('value', keys.split('')));
  }

위와 같은 형태를 띄고있는데요 Actions 과 유사한점이 있지만 다른점이 있다면 바로 Command 를 통해서 실행한다는점입니다.

눈에 띄는 것은 맨 첫줄에 Promise all 부분인데요 들어오는 키 형태중에 조합형태로 Promise 를 리턴하는 경우가 있을 경우에 대비해서 만들어 놓은 것으로 보입니다.

 

위에서 Shift를 눌러놓은 상태에서 automation 시 AUTOMATION 으로 나온후 타 요소에 갈땐 다시 SHIFT가 없는 상태였는데요

 

한 요소에서 같이 될 경우

        await targetElement.sendKeys(Key.SHIFT,'automation');  // AUTOMATION
        await targetElement.sendKeys('ppap'); // ppap

위와같이 보시면 Commander에서 실행 후 다시 초기화가 되는형태로 보여집니다.

 

마지막 궁금했던 테스트입니다.

 await targetElement.sendKeys(Key.CONTROL,'a',Key.CONTROL,'c',Key.CONTROL,'v','v');
 
 await targetElement.sendKeys(Key.CONTROL,'a','c','v','v');
 

그럼 한줄로 그냥 할수 있을거같은데 어떻게 동작하나 보았습니다. 

실제로 이번에 포스팅한 계기는 복사 붙여넣기 자동화 케이스를 만들다가 작성하게 된 경우입니다.

그렇기 때문에 on-copy 와 on-paste 의 이벤트가 Callback 되는지에 중점을 두게 되는데

 

위의 2케이스 전부 콜백은 동작합니다.  v를 한번 더 넣은 경우를 붙여넣기가 블록상태에선 블록만 해제되니까 테스트용으로 두번 넣었습니다. (붙여넣기 확인용)

 

1번 케이스의 경우 결과가 c 로만 나타나게 됩니다.

Ctrl 누르고 a -> 전체 블록 지정    ( 중요하게 보아야 할점은 KeyDOWN  이라는 점 )

다시 Ctrl 누르고 c  ( 한번 더 눌렸기 때문에 keyDOWN Ctrl 이 UP이 되고 c만 입력 )

다시 Ctrl 누르고 v  ( 다시 누르면서 붙여넣기 )

 

결과 : 몇몇 Text 관련 Element 에서는 focus를 주고 난 후에 입력이 되는 경우가 있어서 저는 위처럼 focus를 주고 입력합니다.   

 

긴글 읽어주셔서 감사합니다!!

  // focus in Textarea
        await this.driver.executeScript('arguments[0].focus();',targetElement);
        await targetElement.sendKeys(Key.CONTROL,'a','c','v');

 

+ Recent posts