들어가며

자, 오늘도 TDD에 대한 얘기입니다. :-)


Mars(마르스 or 마즈, 마스 아님!!) version은 ionic2를 지원하죠.

ionic2는 Angular2를 근간으로 하고 있으며, Angular2의 문법은 Typescript입니다. (꿈과 같은 Google과 Microsoft의 콜라보레이션?)

Mars 미국식 [mɑ:rz] 발음 듣기 영국식 [mɑ:z] 발음 듣기 중요

화성


Mars [maʀs] 발음 듣기 

1. 마르스 2. 화성


이런 새로운 환경 하에서 우리는 또 다시 어떻게 하면 TDD를 할 수 있을지를 심각하게 고민해 보아야 합니다.


간단하게 ionic2 test with jasmine으로 검색해 봤습니다.


AngularJs를 공부하면서 신세를 졌던 조쉬 모로니(joshmorony) 사이트가 나오네요.

들어가서 읽어보고 테스트를 해 봅니다.

https://www.joshmorony.com/how-to-unit-test-an-ionic-2-application/


으흠...

테스트 환경을 구축하는 방법이 분리된 post에 정리되어 있네요.

https://www.joshmorony.com/introduction-to-testing-ionic-2-applications-with-testbed/


엄청 깁니다! 그렇지만 포기하지 않고 열심히 따라해 보았습니다.

그런데 이런 된장!! 안되네요...


예제에서 import 하는 일부 class들이 현재와 조금 달라 보입니다.

Angular2 guide에서 찾아보니 이제는 사용하지 않는다네요... OTL


open source라는 게 이렇게 위험합니다 여러분~!! (!true)


...


잠깐 정신을 깨끗하게 하려 해우소에 다녀왔습니다.


어떻게 구글신에게 물으면 원하는 답변을 얻을 수 있을 지 잠깐 고민해 봅니다.

Simple is best!!


'ionic2 unit test' 으로 검색합니다.


ionic2 forum이 보입니다. 아하!!

들어가서 열심히 뒤져봅니다.


건졌습니다!!

http://roblouie.com/article/376/ionic-2-set-up-unit-testing-the-best-way/


몇 가지 수정해야 할 부분이 있지만, guide 거의 그대로 test 가능한 project를 만들 수 있었습니다. :D



Install ionic2

당연히 ionic2 app이 있어야 겠지요.

http://ionicframework.com/getting-started/

물론, 그 전에 node.js가 필요합니다. (# => LTS version을 다운로드 받아 설치해 줍시다.)


install ionic

npm install -g cordova ionic

ionic에는 cordova가 필요하죠. 한 번에 같이 설치하는 명령입니다.


start an app

ionic start {appname} blank

빈페이지를 시작페이지로 갖는 ionic app을 만듭니다. {appname}으로 folder가 생기고 그 아래에 필요한 파일들이 설치됩니다. (대략 150MB)


run your app

ionic serve

app folder로 이동해서 명령어를 실행하면 빈 페이지를 가지는 app이 실행됩니다.



Setup for test

먼저 karma를 설치해야 합니다.

(app folder로 이동해서 실행해주세요.)

npm install -g karma-cli

-g option은 global하게 쓰게 설치하는 option입니다.


그 뒤 테스트에 필요한 module들을 추가로 설치합니다. 

npm install --save-dev @types/jasmine@2.5.41 @types/node html-loader jasmine karma karma-webpack ts-loader karma-sourcemap-loader karma-jasmine karma-jasmine-html-reporter angular2-template-loader karma-chrome-launcher null-loader

뭔가 엄청나게 많은 모듈들이 필요하네요.

가만히 보면 webpack, karma, jasmine 이런 녀석들입니다.


karma를 실행하기 위해서는 몇 가지 configuration이 필요합니다.

보통 config.js파일을 미리 만들어 두고, 이를 로드해서 테스트를 수행하는 형태를 띕니다.

아래 파일을 다운로드 받아서, project folder에 'test-config'라는 folder를 만들고 그 아래에 붙여 넣으세요.


test-config.zip


앞으로 우리가 만들 test code는 실제로 배포를 할 성질의 것이 아니므로, tsconfig.json파일을 수정해서 exclude하도록 합시다.

"exclude": [
"node_modules",
"src/**/*.spec.ts"
],


준비는 모두 마쳤습니다.

이제 test가 잘 되는지 test해 보죠. :-)


karma start test-config/karma.conf.js

참조한 post에서는 해당 명령어를 package.json파일에 선언해 두고, npm test라는 명령어로 수행했는데, 왠지 저는 안되더라고요. 혹시 solution을 아시면 댓글 남겨주세요.


문제 없이 세팅이 되었다면 아래와 같은 메시지를 보실 수 있을 겁니다.

webpack: Compiled successfully.

webpack: Compiling...


그러면, 이제부터는 unit test code를 만들어 보겠습니다.



Write a test

module로 구성된 Angular2의 기능들 중에서 Service라는 녀석은 공용으로 사용할 library같은 녀석입니다.

그런 역할을 가진 만큼 다른 어떤 module 보다도 테스트의 우선순위가 높은 녀석이라고 생각합니다.


그래서 간단한 기능을 가진 serivce를 만들고, 이를 테스트해 보겠습니다.

(Component module은 test하기 어려워서 대는 핑계입니다...)


파일의 위치는 아래와 같습니다.



book.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class BookService {

constructor() { }

public greeting(): string {
return 'hello';
}
}

greeting이라는 function을 호출하면 'hello'를 반환하는 기능 밖에는 없는 단순한 service입니다.

실제 service는 보통 ajax call 등을 수행하고 그 결과를 반환하는 역할을 하겠지요.

우리의 경우는 native call이 될 수도 있고요.


Angular2의 서비스는 어디에든 가져다 쓸 수 있게 끔 @Injectable이라는 데코레이션을 달아줘야 합니다. (.net에서는 비슷한 용도의 코드를 attribute라고 불렀던 것 같은데)


book.service.spec.ts

import { BookService } from './book.service';

describe('Service: BookService', () => {
let service : BookService;

beforeEach(() => {
service = new BookService();
});

it('should be create', () => {
expect(service).toBeTruthy();
});

it('should be say hello', () => {
expect(service.greeting()).toEqual('hello');
});
});

BookSerivce를 참조하기 위해서 import해주었습니다.

서비스는 test 이전에 매번 새로운 instance를 만들도록 beforeEach구문을 통해서 생성해 주었고요.


여기 두 개의 test가 있습니다.

하나는 service가 제대로 instance화 되었는 지를 먼저 확인합니다.

다른 하나는 greeting function이 'hello'를 반환하는 지 확인합니다.


결과가 다음과 같이 나왔습니다.




참고로, test code는 대상 source code와 같은 folder에 위치하고, 그 파일의 이름을 .spec.ts이라고 명명합니다.

왜냐하면, test-config/karma-test-shim.js 파일에 그렇게 찾아서 테스트하라고 정의했거든요.

var appContext = require.context('../src', true, /\.spec\.ts/);


저는 이제부터 M모 방송사의 POC 앱을 만들러 갑니다.


References

http://roblouie.com/article/376/ionic-2-set-up-unit-testing-the-best-way/

'Javascript' 카테고리의 다른 글

Implement base controller  (0) 2017.07.26
Service workers.  (0) 2017.07.06
npm lite-server  (0) 2017.07.06
Calculate distance between two geolocations.  (1) 2017.06.28

Code coverage 측면에서 가능한 모든 코드가 test 되어야겠지만, 사실 여러 가지 요인으로 이 수준을 달성하기가 쉽지 않습니다.

결국은 코드의 중요도를 매기고 그에 따라 선별적으로 TDD 적용을 시작하는 것이 합리적이라고 생각합니다.


그에 따라 AngularJs의 framework에서는 contoller보다는 service 부분이 더욱 TDD가 필요한 부분이라는 생각이 듭니다.

이 글에서는 AngularJs의 service를 테스트하는 방법을 알아 보겠습니다.


우선은 테스트할 대상이 되는 service가 있어야 겠지요.

현재 위치를 확인하는 용도로 gpsService를 만들어 보겠습니다.

getCurrentPosition function은 단말의 GPS 센서를 통해 위치를 가져오거나 navigator.geolocation을 통해서 위치를 가져오도록 구현할 예정입니다.

(참고로 후자의 경우는 protocol이 https가 아니라면 그 요청이 fail처리됩니다.)



gpsService


var app = angular.module('app', []);
app.service('gpsService', ['$q', 'nativeService', function ($q, nativeService) {
this.getCurrentPosition = function () {
var deferred = $q.defer();

// native call.
if (nativeService.isNative()) {
// check
nativeService.getCurrentPosition()
.then(function (position) {
deferred.resolve(new Position(position.lat, position.long);
}, function (fail) {
deferred.reject(fail); // turned off.
});
} else {
var geo = navigator.geolocation;
if (geo) {
try {
geo.getCurrentPosition(function (position) {
var coords = position.coords;
var position = new Position(coords.latitude, coords.longitude);
deferred.resolve(position);
}, function (fail) {
deferred.reject(fail.message);
});
} catch (err) {
deferred.reject(err);
}
} else {
deferred.reject('can\'t detect location via web.');
};
};
return deferred.promise;
};
}]);


promise를 활용하기 위해서 $q를, KEMP native를 연동하기 위하여 nativeService를 참조했습니다.

promise는 async call을 효과적으로 지원하기 위한 spec으로써, 자세한 내용은 link를 참고하시길 바랍니다. (#)

요점만 정리하면 async call에 대해서 성공한 요청은 resolve, 실패한 요청은 reject으로 그리고 기타 알림은 notify로 응답을 전달합니다.

(promise, deferred에 대해서 정말 잘 설명이 되어 있습니다.)


이 서비스는 nativeService를 이용할 수 있는 경우 (즉, service app이 device에서 수행되는 경우)에는 GPS 센서를 통해서 위치를 취득합니다.

그렇지 않은 경우에는 navigator.geolocation을 통해서 위치를 취득합니다.

위치 취득에 성공하는 경우 resolve를 반환하며 반대로 실패하는 경우에는 reject를 반환할 것입니다.

resolve로 전달하는 인자는 Position이라는 별도의 class로 포장하여 일원화하였습니다.



test code


describe('app.gpsService', function () {
// const
var timeout = 800;

// variable
var $rootScope;
var $q;
var gpsService;
var createService;


beforeEach(module('app'));
beforeEach(inject(function (_$rootScope_, _$q_) {
$rootScope = _$rootScope_;
$q = _$q_;
}));
beforeEach(inject(function ($injector) {
createService = function () {
return $injector.get('gpsService');
};
createMockService = function () {
nativeService = $injector.get('nativeService');
nativeService.isNative = function () { return true; };
nativeService.getCurrentPosition = function () {
var deferred = $q.defer();
deferred.resolve(new Position(37.4137701, 127.1284930));
return deferred.promise;
};
return $injector.get('gpsService');
};
}));
it('GPS서비스의 getCurrentPosition이 응답하는지 확인(device가 아닌 경우)', function (done) {
gpsService = createService();
var position, failreason;
var promise = gpsService.getCurrentPosition();
promise.then(function (_position) {
position = _position;
}, function (err) {
failreason = err;
});

setTimeout(function () {
$rootScope.$apply();
if (position) {
expect(position).toBeTruthy();
} else {
expect(failreason).toBeTruthy();
};
console.log('position', position);
console.log('failreaonse', failreason);
done();
}, timeout);
});
it('GPS서비스의 getCurrentPosition이 응답하는지 확인(device인 경우)', function (done) {
gpsService = createMockService();
var promise = gpsService.getCurrentPosition();
var position, failreason;
promise.then(function (_position) {
position = _position;
}, function (err) {
failreason = err;
});
setTimeout(function () {
$rootScope.$apply();
if (position) {
expect(position).toBeTruthy();
} else {
expect(failreason).toBeTruthy();
};
done();
}, timeout);
});
});
});


코드가 좀 기네요...

서비스의 코드량을 가뿐히 능가하고 있습니다. -_-;;


자아, 숨을 가다듬고 천천히 살펴봅시다.


describe는 진입점이라고 생각하시면 됩니다. 두 번째 인자인 function이 실제 역할을 수행하는 부분입니다.


beforeEach

beforEach는 각각의 test를 수행하기 전에 공통적으로 수행할 코드가 담기는 영역입니다.

테스트에 필요한 'app'을 로드하고 $rootScope, $q를 가져오는 기능을 하고 있습니다.


마지막으로 gpsService의 instance가 필요합니다.

이는 세 번째 beforeEach function을 참고하시길 바랍니다.

$injector를 인자로 넘김으로써 AngularJs의 $injector를 참조하고, 그 뒤에 $injector의 get function을 통해서 gpsService를 반환 받았습니다.


it

it의 첫 번째 인자는 test에 대한 설명 구문이며 두 번째 인자가 실제 test코드가 있는 function입니다.

이 function에 선택적으로 전달되는 done 인자는 비동기 function을 테스트하기 위해서 받는 인자이며, 비동기 function의 동작이 마무리 된 이후에 done()을 호출함으로써 테스트를 완료할 수 있습니다.


두 번째 test를 보면 createService() 가 아닌 createMockService()를 통해서 service의 instance를 가져옵니다.

MockService는 테스트를 위해 설정한 natvieService를 반환함으로써 실제 device인 척 가장하고 테스트를 할 수 있게 해 줍니다.

이는 mock을 이용한 테스트 기법입니다.


또한, jasmine을 통해 async function을 테스트하기 위해서는 $rootScope.$apply()을 호출할 필요가 있습니다.

그렇지 않은 경우에는 undefined가 반환됩니다.

왜 $rootScope.$apply()를 호출해야 하는 가에 대해서는 link를 참고하시길 바랍니다. (#)



Conclusion

gpsService의 최초 구조는 위와 같지 않았습니다. 좀 더 많은 function이 있었습니다.

그러나 TDD를 통해서 gpsService를 테스트하면서 gpsService를 참조하는 코드를 작성하면서 복잡함과 불편함을 느끼게 되었고,

이를 통해서 gpsService자체를 refactoring하게 되었습니다.

이 과정에서 nativeService를 분리하였고, @Justin이 그 구현을 도와줌으로써 일에 대한 효율적인 분배도 이뤄낼 수 있었습니다.


잦은 refactoring이 TDD의 핵심입니다.

단순 code coverage만이 목적이라면 개발하기 전에 test를 하라는 개발방법론은 의미가 없습니다.


잦은 refactoring의 원동력은 해당 module을 사용하는 입장이 되어 코드를 작성한다는 점입니다.

Test code는 또한 실제 module을 이용하는 개발자가 참고할 수 있는 훌륭한 sample code가 됩니다.


이렇게 해서 개발한 gpsService는 nativeService와 훌륭하게 연계하여, quickOn POC에 단번에 녹아들어 훌륭하게 동작하였습니다.

이것은 하루를 기분 좋게 만드는 정말로 좋은 경험이었습니다. :)



References

Jasmine expect's matchers => https://jasmine.github.io/api/2.6/matchers.html

inject custom service => https://nathanleclaire.com/blog/2014/04/12/unit-testing-services-in-angularjs-for-fun-and-for-profit/

=> https://coderwall.com/p/l4fvmq/injecting-custom-services-in-an-angularjs-unit-test

get currentPosition => https://developer.mozilla.org/ko/docs/Web/API/Geolocation/getCurrentPosition

setTimeout() method => https://www.w3schools.com/jsref/met_win_settimeout.asp

unit test promise $q => http://www.bradoncode.com/blog/2015/07/13/unit-test-promises-angualrjs-q/

AngularJs $q => https://docs.angularjs.org/api/ng/service/$q

promise controller example code => http://jsfiddle.net/jsengel/8fzmqy4y/

promise? deferred? => http://webframeworks.kr/tutorials/angularjs/angularjs_promise_deferred/

'Dev pattern' 카테고리의 다른 글

[펌] 자바스크립트 코딩 컨벤션 가이드  (0) 2017.10.16
WPF Developement  (0) 2017.07.21
Jasmine getting started to test javascript (Angular)  (5) 2017.05.26

제가 가진 TDD에 대한 관심과 열정은 아직 식지 않았습니다.

미국 현지 프로젝트를 통해서 constructor를 통한 dependency injection pattern을 숱하게 보면서 느낀 점도 많았고요, 아직 갈 길이 멀더라도 TDD는 선택이 아닌 당면한 과제가 된 것 같다는 인상을 받습니다.

 

다만, 계속 아쉬웠던 것은 사실상 현재 주로 작업하는 front-end, javascript에 대해서 (정확하게는 Angular)에 대한 TDD를 시도하지 못하고 있다는 점이었습니다.

1여년전에 도전했다가 angular node.js에 대한 이해 부족으로 미뤄왔던 것을, 이번 ionic2, eletron project를 통해서 node.js(정확하게는 npm)에 대한 경험이 쌓여 다시 시도해 보았습니다.

다행히 전보다는 쉽게 따라갈 수 있었고, 부족하지만 해당 내용을 공유하고자 합니다.

 

도구의 선택

Angular를 테스트하기 위한 최적의 옵션 중 하나는 karma & jasmine입니다.

Karma는 javascript code를 다양한 browser에서 테스트해 볼 수 있도록 도와주는 tool입니다.

JasmineTDD가 아닌 BDD(Behavior driven development) framework로써 자세한 내용은 다음 설명을 참고하세요.


Test-Driven Development : 비교적 새로운 소프트웨어 개발 기술로써, 프로세스는 아래와 같습니다.

 

개발된 코드의 특정 부분을 위한 시험(tests)을 작성합니다. 만약 계산기를 예로 들자면 양수, 음수 정수 등등을 더하는 시험을 작성합니다. 하지만 아직 실제 코드가 작성된 게 아니기 때문에, 테스트를 진행한다면 실패(fail)가 될 것입니다.

이제 시험에 연관된 개발코드를 작성합니다. 이 코드는 테스트를 성공(pass)하기 위한 것입니다.

테스트에서 성공하였다면 이제 작성한 코드를 제 위치에 리팩토링하며 집어넣습니다.

TDD는 개발자들이 개발의 명세(Specification)에 대해 개발을 직접 세밀하게 진행보기 전에, 보다 선명하게 생각할 수 있게 해줍니다. 또한 한번 작성된 시험은 언제나 유용합니다

 

Behavior-Driven Development : BDD는 명세를 작고 쉽게 작성합니다. 기본적으로 BDD는 아래 2개의 중심부분이 있습니다

 

테스트는 반드시 작고 한가지를 테스트해야합니다. 어플레키에션 전체를 테스트하는 대신, 작지만 많은 수의 시험(tests)을 작성합니다. 계산기를 예로 들자면 하나의 테스트는 '1더하기', 하나의 테스트는 '0더하기0', 또 다른 테스트는 '-5더하기6', 그리고 '1.2더하기3.1' 등을 작성하게 됩니다.

시험은 문장이어야 합니다. 자스민프레임워크와 계산기를 예로들면, 문장은 "Calculator adds two positive integers"와 같아야합니다. 그러면 테스팅프레임워크가 자동으로 테스트를 진행할 것 입니다.


출처: http://webframeworks.kr/getstarted/jasmine/

전 아직 위의 두 가지가 정확히 뭐가 다른 지 잘 모르겠네요.

 

Installing

우선 Node.js가 필요합니다.

https://nodejs.org/en/ 에 접속하여 LTS / Current version LTS를 다운로드 하여 설치합니다.

 

npm을 통해서 karma를 설치합니다.

콘솔창에서 다음 커맨드를 실행합니다.

Npm install karma --save-dev

--save-dev 옵션은 package.json을 자동으로 갱신하도록 합니다. 이를 통해서 package.json 파일의 devDependencies에 해당 module이 등록되는 것을 확인할 수 있습니다.

 

기본적인 준비는 마무리 되었습니다.

 

Test code 작성

describe('calculator', function () {

 

    beforeEach(module('myApp'));

 

    var $controller;

 

    beforeEach(inject(function (_$controller_) {

        $controller = _$controller_;

    }));

 

    describe('mainController test', function () {

        it('sample test', function () {

            var $scope = {};

            var controller = $controller('mainController', { $scope: $scope });

            expect($scope.greeting).toBe('Hello world');

            expect($scope.fnTest()).toBe(2);

        });

    });

});

Inject를 통해서 controllerinstance를 가져옵니다.

이 부분은 학습이 더 필요한 영역으로 경험치가 아직 없습니다만, jasmine BDD framework는 문서화가 잘되어 있으므로 손쉽게 따라할 수 있을 것으로 생각합니다.

다만, 위의 코드를 통해서 우리는 특정 controller의 instance를 가져올 수 있습니다.

그리고, 해당 controller의 property나 function을 테스트할 수 있게 되죠.

 

테스트 대상이 되는 controller는 다음과 같습니다.

var myApp = angular.module('myApp', []);

 

myApp.controller('mainController', ['$scope', function ($scope) {

    $scope.greeting = 'Hello world';

    $scope.fnTest = function () {

        return 2;

    };

}]);

 

 

Configuration for test runner

Karma를 통해서 자동 테스트를 수행하기 위해서, test에 대한 설정 파일을 생성해야 합니다.

Karma init karma.conf.js

Test framework > jasmine

Use require.js > no

Browser > Chrome

Location of test code > {test.js 파일의 path} 예를 들면 ‘test/*.js’

 

Run test

테스트를 위해서는 angular / angular-mocks / jasmine-core 등이 필요합니다. 아래의 devDependencies 항목을 package.json 파일에 붙여넣기를 한 후 저장합니다.

{

  "name": "tddtest",

  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

    "test": "echo \"Error: no test specified\" && exit 1"

  },

  "author": "Ethan",

  "license": "ISC",

  "devDependencies": {

    "angular": "^1.6.4",

    "angular-mocks": "^1.6.4",

    "jasmine-core": "^2.6.2",

    "karma": "^1.7.0",

    "karma-chrome-launcher": "^2.1.1",

    "karma-jasmine": "^1.1.0"

  }

}

 

Npm install을 수행하면 package.json에 선언한 module중 필요한 것을 자동으로 다운로드합니다.

 

거의 모든 준비가 완료되었습니다.

마지막으로 실제로 test간 사용할 파일의 karma config값을 수정해야 합니다.

// list of files / patterns to load in the browser

    files: [

      'node_modules/angular/angular.min.js',

      'node_modules/angular-mocks/angular-mocks.js',

      'app/*.js',

      'tests/*.js'

    ],

files arrayangular, angular-mock, source file 그리고 test file을 순서대로 작성합니다.

 

Karma start karma.conf.js를 수행합니다.

마지막 인자는 테스트 환경을 설정 파일을 지정합니다.



 

테스트 코드의 수행 결과가 출력됩니다.

Karma.conf.js 파일의 autoWatch 값을 true로 설정한 경우 Source code, test code 가 변경된 경우 자동으로 탐지하여 테스트 결과를 화면에 출력하여 줍니다.


References

https://www.youtube.com/watch?v=shqptAMZ_NM

https://www.youtube.com/watch?v=YRzr27Bpx_g

http://webframeworks.kr/getstarted/jasmine/

http://www.bradoncode.com/blog/2015/05/19/karma-angularjs-testing/


'Dev pattern' 카테고리의 다른 글

[펌] 자바스크립트 코딩 컨벤션 가이드  (0) 2017.10.16
WPF Developement  (0) 2017.07.21
AngularJs service unit test via jasmine.  (0) 2017.06.29

+ Recent posts