번역

비슷한 주제의 다른 글

Taking Away the Pain From Unit Testing in Google Apps Script

  • GAS의 유닛 테스팅에서 고통 제거하기 ?
  • Google Apps Script 유닛 테스팅 관련 번역
  • 링크 : Taking Away the Pain From Unit Testing in Google Apps Script, medium
  • 2021년 4월 7일 글
  • 저자 : Dmitry Kostyuk
    • Full-time GAS developer,
    • founder at Wurkspaces.dev
      • 큰 회사는 아니고 GAS로 서비스를 만들어주는 업체인듯
  • 댓글이 1개 인데..
    • 저자가 만든 앱과 경쟁관계에 있는 앱 개발자가 달았음.
      • Andrew Roberts : QunitGS2 개발자 (유사한 라이브러리)
        • 흥미있는 기사다.
        • Apps Script 프로젝트가 커지고 복잡해지면서 더욱 유용할것이다
        • 당신 글의 독자들이 흥미있어할만한 다른 솔루션도 있다
        • QUnitGS 소개 링크
      • Dmitry Kostyuk의 답변
        • 댓글 달아줘서 고맙다. Andrew
        • Apps Script 프로젝트의 testing 분야에 대해 같이 talk할 수 있어 너무 좋다(great)
        • 나는 당신의 QUnitGS 보다는 나의 UnitTestingApp을 계속쓰려고 하는데 그 이유는..
          • QUnitGS가 내가 필요로 하는 모두 요구사항을 만족하지는 않기 때문이다..
          • (doesn’t tick all the boxes that I need…) : (체크리스트의) 모든 box를 체크(tick)하지는 못한다
            • 알아둘 필요가 있는 영어 숙어임
  • 번역하다보니, Typescirpt를 쓰는 경우가 고려안된듯 하다. 번역을 계속할지 고민중

Why It’s Crucial

  • Unit testing은 엄청나게 중요한 습관?(practice) 이다
  • 뭔가 고장나자마자 바로 개발자들로 하여금 쉽게 버그를 피하도록 해준다
  • 이것은 또한 리팩토링을 쉽게 만든다
  • Tyler Hawkins가 쓴 최신 article에 이것이 필수적인 이유에 대한 훌륭한 개요 가 있다.
  • 경험이 부족한 개발자들은 급하게 코딩으로 뛰어드는 경향이 있다
    • 그러다가 다시 동작하도록 만들기 전에
    • 버그 때문에 몇시간 혹은 며칠을 소비하기도 한다
  • 당신이 아직 유닛 테스팅에 익숙치 않다면,
    • 여기서 읽기를 멈춰도 된다.
    • 유닛 테스팅에 대해 알고 나서 다시 와라. 기다리겠다
  • Javascript의 unit test를 위한 수많은 라이브러리가 있다.
    • Jest와 Mocha도 여기에 포함된다.
    • 하지만 이 들은 Google Apps Script 환경 아래에 잘 적용되지 않는다
    • GAS를 위한 해결책은 많이 없는데
    • 그것들이 뛰어나게 좋은건 아니다
  • 이런 이유로, Google Apps Script를 염두에 둔 나만의 라이브러리를 개발했다
  • 내가 이런 라이브러리로 부터 어떤 것이 필요한지 궁금했 다. 내 체크리스트를 확인하라
    • 테스트는 로컬 환경과 GAS 환경 양쪽에서 수행되어야 한다
      • 맞다, GAS를 로컬에서 테스팅하는 것은 생각보다 어려운것이 아니다
    • 무겁지 않아야 한다
    • 유지 하기 쉬워야 한다

What’s Inside

  • 라이브러리에 뭐가 있는지 보자
  • UnitTestingApp이라 불리는 작은 클래스가 있다
    • 여기에는 몇가지 간단한 함수가 있다
    • ” 경량과 쉬운 유지보수”를 기억하나?
    • 또한 mock data를 add하고 작업을 하기위한 MockData Class가 있다
      • 이 것은 test를 offline에서 돌리기 위해 중요하다
  • 몇가지 더 있지만 나중에 소개하겠다

Enabling, Disabling, and Checking the Status of the Tests

  • 메소드 두개 : enable(), disable()
  • 프로퍼티 한 개 : isEnabled
  • 가 있다. (희망하건데) 보면 알수 있을 것이다.
  • 문법은 ..
    1
    2
    3
    4
    5
    6
    7
    
    const test = new UnitTestingApp();
    test.enable();
    // code
    if(test.isEnabled) {
    // code
    }
    test.disable();
    

Choosing the Environment with runInGas(Boolean)

  • runInGas(Boolean) 함수로 개발자는
    • 다음줄에 이어지는 테스트가 실행되기를 원하는 환경을 선택할 수 있다
      1
      2
      3
      4
      5
      
      const test  = new UnitTestingApp();
      test.runInGas(false);
      // local tests
      test.runInGas(true);
      // switch to online tests in Google Apps Script environment
      

actual built-in testing methods

  • 실제 내장된 테스팅 메소드는 다음과 같다
assert(condition, message)
  • assert() 는 클래스의 메인 메소드 이다
  • 첫번째로 받는 argument는 조건이다. 참, 거짓을 판단한다
    • condition은 boolean 값이 될수도 있으며
    • boolean을 반환하는 함수가 될수도 있는데
    • error를 잡기위해 함수가 좀 더 선호된다
  • condition이 참이면 “PASSED” 메시지를 로그 출력한다
    • 그외의 경우 “FAILED” 메시지를 로그 출력한다
  • 에러를 던지는 함수를 넘겨주면,
    • 이 method는 에러를 잡고
    • “FAILED” 메시지를 로그 출력한다.
  • 예를 들면:
    1
    2
    3
    4
    5
    
    const num = 6;
    test.assert(() => num % 2 === 0, `Number ${num} is even`);
    // logs out PASSED: Number 6 is even
    test.assert(() => num > 10, `Number ${num} is bigger than 10`);
    // logs out FAILED: Number 6 is bigger than 10
    
catchErr(callback, expectedErrorMessage, message)
  • 이 method의 목표는 당신의 콜백 함수 (callback)가 정확한 에러를 잡는지 테스트한다
  • 당신이 원하는 것은 그 콜백이 실제로 에러를 던지는지 확인하는 것이다
  • 그리고 나서, catchErr() method는 그것이 맞는 것인지 체크할 것이다
  • 마지막으로, 관련 메시지를 로그 출력한다
  • 예를 들면, 어떤 숫자의 제곱값을 반환하는 함수에 숫자가 아닌 값을 입력할 경우 에러를 예상할 때
  • 이런식으로 에러를 테스트 할 수 있다

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    function square(number) {
      if (typeof number !== 'number') throw new Error('Argument must be a number');
      return number * number;
      }
    test.catchErr(
      () => square('a string'), // we're passing a string here to test that our function throws an error
      'Argument must be a number', // this is the error message we are expecting
      'We caught the type error correctly'
      );
    
is2dArray(Array)
  • 이 메소드는 argument가 2D array인지 체크하는 테스트를 실행한다
  • 스프레드 시트 값을 스프레드 시트에 넣기전에 체크하는 용도로 사용된다
    1
    2
    3
    4
    5
    6
    
    const values = [
      ['a1', 'a2'],
      ['b1', 'b2']
      ];
    test.is2dArray(values, 'values is an array of arrays'); 
    // logs out "PASSED: values is an array of arrays"
    
printHeader(headerStr)
  • 헤더를 콘솔에 다음과 같이 뿌려서 가독성을 높인다

    1
    2
    3
    4
    5
    6
    
    test.printHeader('Offline tests');
    /* Logs out the following:
    *********************
    * Offline tests
    *********************
    */
    
clearConsole()
  • 로컬 환경에 있을때 console log를 지우는 직접적인 method 이다
addNewTest(functionName, func)
  • 마지막으로 신규 테스트를 추가하는 메소드이다
  • 예를 들면, 숫자가 짝수인지 검사하는 테스트가 필요하다고 할 때

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    /*
    * Defining our test function and making use of the built-in assert() method to log out a 
    * PASSED or FAILED message
    */
    function isEven(number, message) {
      this.assert(() => number % 2 === 0, message);
      }
    test.addNewText('isEven', isEven);
    const number = 8;
    test.isEven(number, `Number ${number} is even`); // logs out PASSED: Number 8 is even
    
  • 예제에서 이 메소드를 사용할 예정은 아니지만
  • 이 라이브러리를 확장할 방법이 있다는 것은 알아두면 좋다

Thinking the Project Through

  • (역자주) 여기서부터 구체적인 앱을 만들고 테스트를 추가해본다
  • 우리가 구축하려는 app은 간단한 것이다
    • 다가오는 2주동안의 캘린더 데이터를 끌어내어
    • spreadsheet에 저장하고
    • HTML table로 변환하고
    • 이메일로 보내는 것이다
  • 그러면 어떤 클래스가 필요하고
    • 그에 대응되는 어떤 테스트가 필요하게 될까?
    • 하나하나 쪼개어 분석해보자
  • Events class 가 필요하다.
    • start dateend date 두 개의 arguments를 받는다
    • 이 class는 CalendarApp class에 연결되어
    • 두개의 날짜 사이의 모든 예약된 이벤트를 회수할 것이다.
  • 이 것은 구체적으로 아래의 fields를 포함한다
    • start time
    • end time
    • title
    • location
    • guest list
    • our status ( 참석자를 확정했는지 여부)
  • Events class를 위해 아래 tests가 필요하다
    • 2D array를 반환하는지 확인
      • 역자주: 반환되는 데이타가 spread sheet에 저장가능한 2D Array 여야 함
    • 두 개의 arguments중 date 형식이 아닌 데이터가 있는지 확인
  • 일단 class가 기대한대로 작동하는지 확인했다면
    • 출력을 HTML code로 변환해야한다
  • 이 목적을 위해, ArrayToHtml이라는 class를 작성할 것이다
    • ArrayToHtml class 는 2D Array를 입력받아 HTML table 을 출력한다
    • 그러므로, 이 class 가 유효한 HTML table을 반환하는지도 test해야한다

Setting Up the Local Environment

  • local GAS 개발에 대해서는, autocomplete를 지원하는 VS Code가 최고의 IDE이다
  • autocomplete 가 동작하도록 하기 위해서는, node.js와 npm이 설치되어야한다
  • 당신은 또한 아래의 패키지를 설치할 필요가 있다. 터미널에서 아래 명령을 실행하라
    1
    
    > npm install --save @types/google-apps-script
    
  • 이 후에, 로컬 .js 파일을 생성하고, import 'google-apps-script';를 써넣어라
    • 나중에 해당 파일이 온라인 프로젝트에 노출되지 않도록 .claspignore 추가하길 원할 것이다
    • (역자주) Typescript 를 사용하면 이 방법은 필요없을듯…
      • 이 파일은 로컬에서만 존재하는 임시파일인 듯 한데 용도를 정확히 모르겠음.
      • 이 방법은 일반 javascript로 작성할 때 이용할듯 하다
      • TypeScript를 사용하면 이 방법을 안써도 autocompte이 잘 된다
  • 당신이 설치해야 하는 또 다른 패키지는 clasp이다.
    • 아래 명령을 터미널에서 실행하라
      1
      
      > npm install @google/clasp -g
      
  • 이제 당신 코드를 서버와 동기화 시키기 위해 아래 명령을 사용한다
  • 내가 일반적으로 사용하는 방법은 다음과 같다
    1. Google Apps Script 프로젝트를 온라인에서 먼저 생성한다
    2. ID 를 복사한다
    3. 프로젝트를 저장할 로컬폴더를 생성하고 터미널에서 해당 폴더로 이동한다
    4. 로그인 안되어 있거나, 다른 계정으로 로그인 되어 있다면 clasp login을 먼저 실행한다
    5. clasp clone "<PROJECT_ID>" 를 실행하여, 온라인 프로젝트를 로컬 폴더에 동기화 한다
    6. clasp push -w를 실행한여 로컬 폴더를 수정하고 저장할 때마다 자동으로 온라인에 동기화 한다
    7. 작업을 끝내고 온라인에 code를 pushing 하는 것을 멈추기 위해, Ctrl+c를 누른다
  • 로컬 파일들은 .js 확장자를 가져야 한다.
    • 온라인으로 동기화 될 때 Apps Script의 native인 .gs 확장자로 변경된다
    • 좀더 자세한 내용은 clasp 매뉴얼을 참조하라

Setting Up the Tests Environment

  • 당신이 코드를 작성하기 전에 테스트를 작성하는 것은 좋은 습관이다
    • 처음에는 FALIED 결과를 내는 테스트로 채워지겠지만
    • 코드를 작성해 감에 따라 점차 PASSED로 채워질 것이다
  • 엣지 케이스를 포함하여 모든 테스트가 완전하게 통과되면
    • 당신이 작성한 코드가 안정적임을 확인할 수 있다
  • git을 설치했다면, git clone을 실행해 나의 repository를 clone 하라
  • git을 설치하지 않았다면, 나는 나중에라도 설치하길 권장하지만
    • 일단 지금은, 해당 리포지토리로 가서 zip파일을 다운로드 받아서
    • UnitTestingApp.js, MockData.js, TestingTemplate.js를 로컬에 설치한다
  • 위 3 파이을 당신의 프로젝트 폴더에 복사하라
    • TestingTemplate.js 파일은 Tests.js 로 이름을 변경해도 된다
  • 이제, testing template을 한 번 보자
  • 이 곳이 모든 magic이 일어나는 곳이다

    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
    
    if (typeof require !== 'undefined') {
     UnitTestingApp = require('./UnitTestingApp.js');
    }
       
    /*****************
    * TESTS
    *****************/
       
    /**
    * Runs the tests; insert online and offline tests where specified by comments
    * @returns {void}
    */
    function runTests() {
     const test = new UnitTestingApp();
     test.enable();
     test.clearConsole();
       
     test.runInGas(false);
     test.printHeader('LOCAL TESTS');
     // Local tests go here
       
     test.runInGas(true);
     test.printHeader('ONLINE TESTS');
     // Online tests go here
    }
       
    /**
    * If in a local environment, executes the tests. In a GAS environment, runTests() must be executed manually
    */
    (function() {
     /**
    * @param {Boolean} - if true, we're in a GAS environment, otherwise we're running locally
    */
     const IS_GAS_ENV = typeof ScriptApp !== 'undefined';
     if (!IS_GAS_ENV) runTests();
    })();
    
  • 위 코드에서 어떤일이 일어나는지 보자
  • 첫번째 라인에서
    • UnitTestingApp.js 파일을 요구했다
    • if (typeof require !== 'undefined') 문구는 require를 로컬 환경에서만 실행하도록 확정한다
    • require는 Apps Script 환경에서는 없기때문에 if문으로 에러를 던지지 않도록 조치한다
  • runTests() 함수에 모든 testing 코드가 있다
    • 그 안에서, enable()로 테스트를 활성화 하고
    • clearConsole()로 이전 테스트 결과를 지운다
    • 그 다음엔, runInGas()로 시작하는 두 개의 코드 블럭이 있는데 각각 offline과 online 테스트를 수행한다
    • printHeader()로 어떤 환경에서 실행되고 있는지 정확히 볼수 있다
  • 파일 마지막 부분의 IIFE는 Apps Script 환경에 있는지 여부를 체크한다
    • Apps Script 환경이 아니라면, runTests() 함수를 실행한다.
    • GAS online IDE 환경이라면, runTests() 함수는 수동적으로 실행해야 한다
  • 이제 우리가 해야할 것은 “offline tests”와 “online tests”라는 헤더 아래쪽에 테스트 코드를 넣는 것 뿐이다

Writing the Tests

  • 이전 섹션에서, 우리가 어떤 테스트가 필요할지 이미 결정했다
  • offline 테스트 블럭에 실제로 테스트 코드를 넣어보자
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    const now = new Date();
    const later = new Date(new Date().setDate(now.getDate() + 14));
    const events = new Events(now, later);
       
    test.catchErr(
      () => new Events('not a date', new Date(2021, 11, 31, 23, 59, 59)),
      'startTime is not a valid Date object',
      'startTime type error successfully caught'
    );
       
    test.catchErr(
      () => new Events(new Date(2020, 0, 1, 0, 0, 1), 'not a date'),
      'endTime is not a valid Date object',
      'endTime type error successfully caught'
    );
       
    test.is2dArray(() => events.get(), 'Calendar data is a 2D array');
    test.assert(() => events.get().length > 3, 'Calendar array has multiple rows');
    
  • 처음 두개의 메소드는 catchErr() 이며, 각각 새로운 Events 객체를 생성한다
  • 각각은 date 대신 문자열 argument를 넘기고 있다
  • 이 테스트에서 Events class가 에러를 던지길 기대한다
  • 그리고 나서, Eventsget() method가 2D array를 반환하는지 체크하고 싶기 때문에
    • is2dArray() method로 입력한다
  • 마지막으로, 적어도 몇개 이상의 events가 있는지 2D array의 길이를 assert()로 체크한다
  • 터미널에서 프로젝트 폴더로 이동해서
    • nodemon Tests.js(nodemon을 설치하지 않았으면 node Tests.js)를 실행한다
    • 이 시점에서 모든 tests는 다음과 같이 fail이 난다
    • 테스트 실패 결과

Creating the Events Class

  • 이제, Events class를 new Events(startDate, endDate)로 초기화 할 수있고
  • get() method로 data를 2D array 형식으로 회수할수 있기를 원한다는 것을 안다
  • 당신이 작성한 class의 내용을 출력하기 위해 class에 print() mothod를 추가하는 것도 좋은 습관이다
  • 이 method는 나중에 필요할 것이다
  • 한 번 만들어 보자
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    let Events = (function() {
      const _events = new WeakMap();
    
      class Events {
       constructor(startTime, endTime, calId) {
         if (!(startTime instanceof Date)) throw new Error('startTime is not a valid Date object');
         if (!(endTime instanceof Date)) throw new Error('endTime is not a valid Date object');
       }
       get() {
       }
       print() {
       }
      }
        
      return Events;
    })();
    
    if (typeof module !== 'undefined') module.exports = Events;
    
  • Events class 는 자체 파일로 이동해야 한다
  • edge case로 시작해보자
    • constructor는 두 개의 arguments를 받는다: startTime, endTime
    • 우선, startTimeendTimeDate의 인스턴스인지, 아니면 error를 던지는지 체크하고 싶다
    • 이미 test file에서 에러 메시지를 정의했으므로 사용해보자
  • 모듈을 module.exports로 export해서 test file이 import 할수 있도록 하자
  • (역자) 번역은 일단 여기서 중단 다른 Test Framwork을 써보기로 했음

Effortless Ctags with Git

  • 원문 링크 : Effortless Ctags with Git, tpope blog
  • Git 으로 Ctags 쉽게 하기 ?
  • 저자 : 그 유명한 Tim Pope
  • 작성일 : 2011년 8월 8일
  • 요즘 Ctags를 많이 만나는데 번역해보기로 했다
  • 참고로 Exuberant Ctags은 2009년에 업데이트가 멈췄는데 2011년의 Tim Pope는 아직 모르는 듯

본문

  • 프로그래밍 바위 (programming rock) 아래에서 살아왔다면, (즉, 프로그래밍에 파묻혀 살아왔다면)
    • (역자주) Living under a rock 은 일종의 숙어로 ..
      • (어디 처박혀 사느라고) 밖에서 무슨일이 일어나는지도 모른다는 의미를 내포한다
  • Ctags은 (Exuberant Ctags를 말하는거다. OS X 와 함께 배포되는 BSD version이 아님) ..
    • (여러 에디터들중에서) Vim (:help tags를 봐라)에서 함수들, 변수들, 클래스들과 다른 identifiers간에
    • 점프하는것이 쉽도록 소스코드를 인덱싱한다.
  • Ctags의 가장 큰 단점 (major downside)은 ..
    • 그 인덱스를 매번 수동으로 rebuild 해야 하다는 거다
  • 이 지점에서 그렇게 참신하지 않은, 다양한 Git commit hook에서 re-indexing 을 하자는 아이디어가 왔다
    • (역자주) 위에서 그렇게 참신하지 않은에 링크가 걸려있는데 지금은 끊겨있다
      • 이 링크 를 보니 다른 사람이 이미 만든 아이디어인듯 하다
  • Git hook은 리포지토리에 따라 다르다 (repository specific)
    • 설치할 때 스크립트 사용하기를 추천했던 어떤사람이 주어진 리포지토리에 hook을 넣으라고 말했다
    • 하지만 내게는 그 건 너무 수작업적이다
    • Git 이 repository를 생성하거나 클로닝할 때 템플릿으로 쓸만한 기본적인 hook 세트를 만들어 보자
      • Git 1.7.1 이상 버전이 필요하다
        > git config --global init.templatedir '~/.git_template'
        > mkdir -P ~/.git_template/hooks
        
  • 첫번째 hook을 보면, 실제로 hook이 전혀 아니고, 다른 hook이 호출할 스크립트라고 할수 있지만
    • .git_template/hooks/ctags 으로 가서
    • 실행가능한 것으로 mark 해라 ?? ( linux 의 chmod 명령으로 hook을 executable로 변경하라고 하는 듯)
    • (역자주)이와 유사한 코드를 gist에서 발견함
  • 번역은 일단 여기서 중단함
    • 모르는 개념이 너무 많음
    • git hook을 공부하고 추후 진행해볼 것

Google Apps Script Testing Framework

기타