터미널 출력 자체를 Mocha로 테스트하기

test-console

비동기식 함수를 mocha와 chai로 테스트 하기

  • #mocha, #chai
  • Open API Test 에서 api 호출함수들은 비동기식으로 동작한다
  • 때문에 바로 결과를 확인할 경우 결과가 안나올수 있다.
  • 관련 내용이 있는지 찾아봤다.
  • Modern Node.js: async/await based testing with Mocha & Chai
    • 2017년 4월 9일 내용임

번역 - Modern Node.js: async/await based testing with Mocha & Chai

  • #translation
  • Mocha는 Node.js 환경이나 브라우저환경에서 실행되는 Javascript 테스트 프레임웍이다
  • Mocha는 비동기 와 동기 코드를 직렬로(serially) 모두 실행할 수 있다.
  • 테스트 케이스는 describe()it() 메소드를 이용하여 생성된다.
    • describe()는 논리적으로 그루핑할 수있는 다양한 케이스를 포함하는 구조를 제공하는데 사용되고
      • it()는 테스트 자체가 쓰여진다
  • 실제 테스트를 수행하기 위해, assertion 라이브러리가 필요하다
    • assertion 라이브러리는 프로그램에 의해 가정된 결과를 검증하기 위해 쓰이는 실행 메커니즘이다.
    • assertion 라이브러리는 또한 가정이 fail이 나면 진단 메시지를 출력한다.
  • Node.js는 내장 assert 라이브러리가 있지만 …
    • Chai는 테스팅코드에 BDD혹은 TDD스타일의 프로그래밍을 모두 제공하는 인기있는 라이브러리이다.
    • BDDBehavior-driven development를 의미한다.
    • TDDTest-driven development를 의미한다.
    • 간단히 말하면, Chai는 BDD 스타일을 위해 chain assertion을 허용하는 should 키워드를 제공한다.
    • 또한, TDD 스타일을 위해 expect()를 지원한다.
    • 둘 중에 어떤 스타일을 선택할 지는 개인의 선호도에 따른다.
  • should를 사용한 BDD 스타일 예제 ( 좀더 영어문장스럽다.)
    1
    2
    3
    4
    5
    6
    
    chai.should();
      
    foo.should.be.a('string');
    foo.should.equal('bar');
    foo.should.have.lengthOf(3);
    tea.should.have.property('flavors').with.lengthOf(3);
    
  • expect를 사용한 TDD 스타일 예제
    1
    2
    3
    4
    5
    6
    
    const expect = chai.expect;
      
    expect(foo).to.be.a('string');
    expect(foo).to.equal('bar');
    expect(foo).to.have.lengthOf(3);
    expect(tea).to.have.property('flavors').with.lengthOf(3);
    
  • async/awit를 이용한 비동기 코드를 테스트하기 위해 한군데 몰아넣자.
    • 여기에 비동기 add() 함수가 있다.
    • Mocha와 Chai를 expect 스타일로 조합하고 async/await ES7 문법을 사용한다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      
      const expect = require('chai').expect;
          
      async function add(a, b) {
        return Promise.resolve(a + b);
      }
            
      describe('#add()', () => {
        it('2 + 2 is 4', async () => {
          const p = await add(2, 2)
          expect(p).to.equal(4);
        });
        it('3 + 3 is 6', async () => {
          const p = await add(3, 3)
          expect(p).to.equal(6);
        });
      });
      
  • async/await 문법으로 chai-as-promised 같은 플러그인은 더이상 필요없어졌다.
    • 프로미스에 대해 fact를 assert 할 필요가 없어졌기 때문
    • 단순히 값들만 다루면 된다.
  • Mocha에 lambda나 arrow function을 넘기는 것도 추천하지 않는다.
    • Mocha helper function( 어휘적으로 lexically this에 결합됨) 에 접근하는 것이 불가능하기 때문에..
    • 사실상 그런 함수들은 거의 쓰이지 않음.
  • 실행하기위해 Node.js 프로젝트를 생성하자
    1
    2
    3
    
    > mkdir async-await-mocha-chai-example
    > cd async-await-chai-example
    > yarn init -y
    
    • (역자주) yarn은 npm 비슷한 패키지 매니저인데 npm의 단점을 개선하려고 만듬
  • 다음은, mochachai를 dev dependency로 설치한다.
    1
    
    > yarn add mocha chai --dev
    
  • 마지막으로 테스트코드를 test/add.test.js 안에 두고 실행해보자
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    > yarn run mocha
      
    yarn run mocha
        
      #add()
        √ 2 + 2 is 4
        √ 3 + 3 is 6
        
      4 passing (199ms)
        
      Done in 0.99s.
    
  • 좀 더 편하게, test명령을 package.jsonscripts 섹션에 두어도 된다.
    1
    2
    3
    
    "scripts": {
      "test": "mocha --reporter list"
    }
    
  • 이런 방식으로 yarn run test 와 같이 테스트를 실행할수 있다.
    • --reporter와 같은 기본옵션을 한번만 설정하면 모든 테스트에 동일하게 적용가능하다.
    • 어떤 Mocha 테스트가 -g <pattern>이나 -f <substring> 옵션을 사용할지 선택할 수 있다.
  • Mocha는 before()after()같은 hook 메소드도 지원한다.
    • 이런 hook은 모든 테스트의 사전과 사후의 외부 자원을 관리(설정과 해체)하는데 사용된다.
    • 특정 테스트용으로는 beforeEach()afterEach()가 있다.
  • 데이타베이스 지속성을 테스트하는 예제가 있다.
    • 데이타베이스 연결은 모든 테스트전에 한번만 수행하면 된다.
    • 데이타베이스는 각 테스트 전에 처음부터 초기화되므로 각각의 테스트는 동일한 초기 상태에서 시작하게된다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      
      const expect = require('chai').expect;
      const Sequelize = require('sequelize');
          
      describe('users', () => {
        let database;
        let User;
            
        before(async () => {
          database = new Seuelize('postgresql://localhost/app_test', { logging: false });
          User = database.define('user', {
            usernaem: Sequelize.STRING,
            birthday: Seuelize.DATE
          });
        })
            
        beforeEach(async () => {
          await User.sync();
          await User.create({
            username: 'zaiste', 
            birthday: new Date(1988, 1, 21) 
          });
        })
            
        afterEach(async () => {
          await User.drop();
        })
            
        describe('#find()`, () => {
          it('should find a user', async () => {
            const user = await User.findOne({ where: { username: 'zaiste' }})
            expect(user).to.be.a('object');
            expect(user).to.have.property('username');
            expect(user).to.have.propety('birthday');
            expect(user.username).to.equal('zaiste');
          });
        });
      });
      
  • 마지막으로 Mocha는 테스트 케이스 옆에 잡히지 않은 예외를 출력하여..
    • .. 각 테스트별로 실패한 곳과 그 이유를 구분되도록 합니다.

html을 parse 하는 library의 비교

Parsing HTML: A Guide to Select the Right Library, STRUMENTA

  • #translation
  • 언어별 HTML을 파싱하는 라이브러리를 소개하는 내용
  • 2017년 9월 21일자 내용
  • 알아야할 용어
    • WHATWG : 웹 하이퍼텍스트 애플리케이션 테크놀로지 워킹 그룹
    • WHATWG DOM & HTML : 위 단체에서 정한 웹 규격

NODE.js

  • Node.js는 web으로 쉽게 작업할수 있지만,…
    • 브라우저처럼 손쉽게 파싱 기능을 사용할 수 있는건 아니다.
    • 이런 의미에서, Node.js의 JavasCript는 전통적인 언어와 비슷한 입장이다.
    • 즉, parsing 작업은 개발자 자신이 직접 처리해야한다.

Cheerio

서버를 위해 특별히 설계된 core jQuery의 빠르고, 유연하며, 간결한 구현

  • Cheerio서버에서 동작하는 jQuery 이다.
  • 이것이 명확한 설명이지만, 어쨌든 좀 더 설명하겠다
    • jQuery처럼 보이지만, 브라우저가 존재하지 않는다.
    • 즉, Cheerio는 HTML을 파싱하고 다루기 쉽게하지만, 실제 렌더링이 일어나지는 않는다.
    • 브라우저에서 하는 것처럼 HTML을 처리하지 않는다.
      • 브라우저와 다르게 구문을 분석한다는 의미에서나..
      • 분석 결과가 곧장 사용자에게 전달되는게 아니라는 점에서 모두 브라우저와는 다르다
      • 그런 작업이 필요하다면 사용자가 직접 처리해야한다.
  • Cheerio는 몇가지 jQuery 유틸리티 함수를 포함한다.
    • sliceeq 같이 범위를 조절하기 위한 함수이다.
    • 데이타를 배열 이름과 form elements의 값으로 serialize 할 수 있지만,
    • jQuery가 하는 것처럼 server에 그것들을 제출할 수는 없다.
      • Node.js가 서버상에서 동작하기 때문이다.
  • 개발자는 jsdom의 가벼운 버전의 대체제를 원했기 때문에 이 라이브러리를 개발했다.
    • 이 라이브러리는 또한 더 빠르고 파싱에 덜 엄격하다.
    • 실제적이고 지저분한 웹사이트를 파싱하기 위한 이유도 있습니다.
  • Cheerio 의 문법은 JavaScript 개발자에게 아주 익숙합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    
    var cheerio = require('cheerio'),
      $ = cheerio.load('<h3 class = "title">I am here~</h3>');
        
    $('h3.title').text('There i nobody here!');
    $('h3').attr('id', 'in_hiding');
      
    $.html();
    //=> <h3 class = "title" id="in_hiding">Thfer is nobody here!</h3>
    
  • 위 설명은 프로젝트의 긴 README 보다 제한적이지만, 당신이 필요한 설명이 대부분 있을것이다.

Jsdom

jsdom 은 Node.js와 함께 사용하기 위한 많은 web 표준, 특히 WHATWG DOMHTML 표준의 순수 Javasciprt 구현이다. 일반적으로 이 프로젝트의 목표는 실제 웹 애플리케이션을 테스트하고 스크래핑하는 데 유용 할 수 있도록 웹 브라우저의 하위 집합을 충분히 에뮬레이션하는 것입니다.

  • 그래서 jsdom은 HTML parser라기 보다는 브라우저처럼 동작합니다.
    • 파싱의 context에서 jsdom은 당신이 파싱하려는 데이타에서 필요한 태그를 누락한다면 jsdom이 자동적으루 추가한다는 의미이다.
    • 예를들어 html tag가 없다면 브라우저가 의례 그렇듯 묵시적으로 html tag를 추가한다.
  • DOM 표준을 지원한다는 것은
    • jsdom object가 documentwindow처럼 친숙한 속성을 가지며
    • DOM을 다루는 것이 일반 javascript를 사용하는것과 다를바 없을것을 의미한다.
  • document, referrer, user agent의 URL과 같은 몇가지 속성을 선택저으로 지장할 수도 있다.
    • URL은 로컬 URL이 포함된 링크를 분석해야할 경우 특히 유용하다.
  • 파싱과 실제로 관련이 없기 때문에, jsdom이 (가상) console, 쿠키 지원이 존재한다고만 언급한다.
    • 간단히 말해서 브라우저 환경을 시뮬레이션하는데 필요한 모든 것이다.
    • 그것은 또한 외부 리소스, 심지어 javascript 스크립트도 처리할 수 있다.
    • 즉, 당신이 원한다면 (자바스크립트를) 로드하고 실행시킬수도 있다.
    • 어떤 외부 코드를 실행할 때라도 항상 그런것처럼 보안상의 이슈가 있을수 있음에 주목해라
    • 문서상에 관련 주의사항이 있으니 읽어볼것
  • 주목할 또다른 내용은 파싱이 일어나기전 환경을 변경할 수 있다는 점이다.
  • 예를들면, jsdom 파서가 지원하지 않는 기능을 시뮬레이트할 Javascript를 추가할 수 있다.
  • 이런 라이브러리들은 shims라고 불린다.
    1
    2
    3
    4
    5
    
    const jsdom = require("jsdom");
    const { JSDOM } = jsdom;
    const dom = new JSDOM('<!DOCTYPE html><p>Goodbye world</p>');
      
    console.log(dom.window.document.querySelector("p").textContent);
    
  • 관련 문서는 충분히 좋다.
  • 프로젝트의 방대함에 비해 놀라울 정도로 짧지만,
    • DOM을 사용하기 위한 문서들은 다른곳에서 찾을 수 있기에
    • 짧아도 그럭저럭 넘어갈수 있다.
  • Felix Böhm
    • HTML (XML and RSS) , CSS selecter 를 파싱하고
    • DOM 을 구축하는 몇가지 라이브러리를 만들었다.
    • 매우 성공적이었고, 심지어 cheerio 라이브러리를 강력하게 하는데도 충분히 좋다.
    • 라이브러리는 분리하여 사용해도 되지만, 함께 사용될 수 있다.
  • HTML 파서는 빠르지만, 매우 기본적이다.
  • 아래 예제는 태그나 텍스트 요소를 만날때 단지 함수만 실행시키면 되는것을 보여준다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    // from the documentation
    var htmlparser = require("htmlparser2");
    var parser = new htmlparser.Parser({
      onopentag: function(name, attribs){
        if(name == "srcipt" && attribs.type === "text/javascript"){
          console.log("JS! Hooray!");
        }
      },
      ontext: function(text) {
        console.log("-->", text);
      }
    }, {decodeEntities: true});
    parser.write("Xyz <script type='text/javascript'>var foo = '<<bar>>';</script>");
    parser.end();
    
  • HTML 문서의 복잡한 고급 조작이 필요한 경우 강력하고 훌륭하다.
  • 하지만, 간단한 HTML 파싱이나 간단한 DOM 조작을 하려고할 때는,
    • 라이브러리를 함께 사용하더라도 다소 투박하다.
    • 부분적으로, 이것은 기능 자체 때문이다.
    • 예를 들면, DOM 라이브러리는 단지 DOM을 빌드만하며, 이것을 다루는 helper함수가 없다.
    • 실제로 DOM을 조작하려면 domutils라는 또 다른 라이브러리가 필요한데,
      • domutils는 문자그대로 전혀 문서화가 안되어 있다.
  • 하지만, 진짜 이슈는 각 기능위에서 통합하는 기능은 제공하지 않으며, 단지 같이 동작하기만 한다는 것이다
    • 대부분 고급 구문 분석 요구를 위해 설계되었다.
    • 예를 들면, 내부에서 HTML을 사용하는 워드프로세서를 만들려고 할때는 대단히 유용하다.
    • 이런 목적이 아니라면, 다른 것을 찾아봐야할것이다?
  • 이런 사용하기 어려운점은 제한된 문서화때문이다. 좋은 부분은 CSS selector 엔진뿐이다.

Parse5

parse5는 당신이 HTML을 다룰 때 필요한 거의 대부분의 기능을 지원한다.

  • Parse5는 다른 도구를 만들기 위한 라이브러이지만 간단한 HTML 분석에 직접사용할 수도 있다.
  • 하지만 두번째 용도에 대해서는 다소 제한적이다. 아래 예제를 보자
    1
    2
    3
    4
    
    const parse5 = require('parse5');
      
    const document = parse5.parse('<!DOCTYPE html><html><head></head><body>Hi there!</body></html>');
    console.log(document.childNodes[1].tagName); //=> 'html'
    
  • 사용하기는 쉽지만, 이슈는 브라우저가 DOM을 다룰 때 제공하는 도구를 제공하지 않는데에 있다. (예를 들면, getElementById)
  • 제한된 문서화로 어려움이 더 증가한다: API reference 관련 질문에 대한 답변의 연속으로 이루어져있다.
  • (예를 들면, “HTML 문자열을 파싱하고 싶다” => parse5.parse 메소드를 사용하라)
  • 따라서 간단한 DOM 조작에 사용하는 것이 가능은 하지만 아마도 당신이 그것만 원하지는 않을것이다.
  • 반면에 parse5는 jsdom, Angular2및 Polymer와 같은 인상적인 프로젝트 시리즈를 나열한다.
  • 따라서 HTML의 고급 조작 또는 구문 분석을 위한 신뢰할 수 있는 기반이 필요한 경우 분명히 좋은 선택이다.