2021년 35주차 기록
2021.08.30
unit-testing 악보
08/30 (월) ~ 09/05 (일)
-
번역
- 비슷한 주제의 다른 글
- Taking Away the Pain From Unit Testing in Google Apps Script
- Effortless Ctags with Git
- Google Apps Script Testing Framework
- 기타
번역
비슷한 주제의 다른 글
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)하지는 못한다
- 알아둘 필요가 있는 영어 숙어임
- Andrew Roberts : QunitGS2 개발자 (유사한 라이브러리)
- 저자가 만든 앱과 경쟁관계에 있는 앱 개발자가 달았음.
- 번역하다보니, Typescirpt를 쓰는 경우가 고려안된듯 하다. 번역을 계속할지 고민중
Why It’s Crucial
- Unit testing은 엄청나게 중요한 습관?(practice) 이다
- 뭔가 고장나자마자 바로 개발자들로 하여금 쉽게 버그를 피하도록 해준다
- 이것은 또한 리팩토링을 쉽게 만든다
- Tyler Hawkins가 쓴 최신 article에 이것이 필수적인 이유에 대한 훌륭한 개요 가 있다.
- 경험이 부족한 개발자들은 급하게 코딩으로 뛰어드는 경향이 있다
- 그러다가 다시 동작하도록 만들기 전에
- 버그 때문에 몇시간 혹은 며칠을 소비하기도 한다
- 당신이 아직 유닛 테스팅에 익숙치 않다면,
- 여기서 읽기를 멈춰도 된다.
- 유닛 테스팅에 대해 알고 나서 다시 와라. 기다리겠다
- Javascript의 unit test를 위한 수많은 라이브러리가 있다.
- Jest와 Mocha도 여기에 포함된다.
- 하지만 이 들은 Google Apps Script 환경 아래에 잘 적용되지 않는다
- GAS를 위한 해결책은 많이 없는데
- 그것들이 뛰어나게 좋은건 아니다
- 이런 이유로, Google Apps Script를 염두에 둔 나만의 라이브러리를 개발했다
- 내 리포지토리를 체크해라 . UnitTestingApp
- 내가 이런 라이브러리로 부터 어떤 것이 필요한지 궁금했 다. 내 체크리스트를 확인하라
- 테스트는 로컬 환경과 GAS 환경 양쪽에서 수행되어야 한다
- 맞다, 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 date 와 end 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 형식이 아닌 데이터가 있는지 확인
- 2D array를 반환하는지 확인
- 일단 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
- 아래 명령을 터미널에서 실행하라
- 이제 당신 코드를 서버와 동기화 시키기 위해 아래 명령을 사용한다
- 내가 일반적으로 사용하는 방법은 다음과 같다
- Google Apps Script 프로젝트를 온라인에서 먼저 생성한다
- ID 를 복사한다
- 프로젝트를 저장할 로컬폴더를 생성하고 터미널에서 해당 폴더로 이동한다
- 로그인 안되어 있거나, 다른 계정으로 로그인 되어 있다면
clasp login
을 먼저 실행한다 -
clasp clone "<PROJECT_ID>"
를 실행하여, 온라인 프로젝트를 로컬 폴더에 동기화 한다 -
clasp push -w
를 실행한여 로컬 폴더를 수정하고 저장할 때마다 자동으로 온라인에 동기화 한다 - 작업을 끝내고 온라인에 code를 pushing 하는 것을 멈추기 위해, Ctrl+c를 누른다
- 로컬 파일들은
.js
확장자를 가져야 한다.- 온라인으로 동기화 될 때 Apps Script의 native인
.gs
확장자로 변경된다 - 좀더 자세한 내용은 clasp 매뉴얼을 참조하라
- 온라인으로 동기화 될 때 Apps Script의 native인
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()
함수는 수동적으로 실행해야 한다
- Apps Script 환경이 아니라면,
- 이제 우리가 해야할 것은 “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가 에러를 던지길 기대한다 - 그리고 나서,
Events
의get()
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
- 우선,
startTime
과endTime
이Date
의 인스턴스인지, 아니면 error를 던지는지 체크하고 싶다 - 이미 test file에서 에러 메시지를 정의했으므로 사용해보자
- constructor는 두 개의 arguments를 받는다:
- 모듈을
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
- Git 1.7.1 이상 버전이 필요하다
- 첫번째 hook을 보면, 실제로 hook이 전혀 아니고, 다른 hook이 호출할 스크립트라고 할수 있지만
- .git_template/hooks/ctags 으로 가서
- 실행가능한 것으로 mark 해라 ?? ( linux 의 chmod 명령으로 hook을 executable로 변경하라고 하는 듯)
- (역자주)이와 유사한 코드를 gist에서 발견함
- 번역은 일단 여기서 중단함
- 모르는 개념이 너무 많음
- git hook을 공부하고 추후 진행해볼 것
Google Apps Script Testing Framework
기타
- 악포 프린트 ( #악보)