블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2013. 10. 23. 17:48 Testing, TDD/Test First

AngularJS를 위한 Test 수행 방법을 생각해 보고, 테스트 전략에 대해서 다른 개발자는 어떻게 하고 있는지 알아보자 


  



1. Angular Testing 에 대한 고찰

 1-1. 스토리 보드 만들기 

  - 프로젝트 진행이 Agile Scrum 이라면 Story를 먼저 도출한다 

  - Story가 도출되었다면 스토리안에 Task를 나눈다 그리고 Task 별로 Validation(검증) 및 Processing(처리)해야할 것을 명세한다

  - 명세는 스토리카드안에 간단히 적는다. 


 1-2. UX&UI 디자인

  - 스토리에 맞는 화면의 목업(Mockup or WireFrame)을 그린다. Balsamiq같은 목업툴을 이용한다 

  - 화면 목업이 스토리에 부합되는지 확인 되면 스토리 개발 우선순위를 결정한다

  - 결정된 화면의 HTML을 Bootstrap 또는 BootFlat을 이용하여 코드를 만든다. 저작툴이 Bootply를 이용해도 된다 


 1-3. TDD 수행하기 개발

  - AngularJS 기반의 개발시 다음과 같이 진행을 한다 (AngularWay 참조)

    + View  === AngularJS의 Directive가 포함된 HTML이다 

    + Model === $scope를 통하여 Controller에서 Two way binding을 자동 수행한다 

    + View와 Controller를 통하여 표현과 행위를 분리한다 (SoC)

    + AngularJS 애플리케이션 접근법

      Step 1) Model을 먼저 생각하고 Service를 만든다

      Step 2) Model이 표현될 View에 대해 생각해 보고 template을 만들고, 필요하면 자동 모델 바인딩되는 Directive도 만든다.

      Step 3) 각 View 를 Controller에 연결한다 (ng-view and routing, ng-controller)


  - E2E Testing

    + AngularJS의 Config의 Routing 에 대해 E2E Test 수행 

    + Model을 표현하는 template에 대해 E2E Test 수행 

    + Directives를 HTML에 코딩하였다면 E2E Test 수행 

    + Filter가 HTML에 표현되는 것이라면 E2E Test 수행

    (Protractor 또는 Angular Test Scenario Runner 사용)

  - Unit Testing

    + Model을 서버 또는 Controller로 부터 받으면 Service/Factory에 대하여 Validation과 XHR등 스팩에 대한 Unit Test 수행

    + Model을 가공 핸들링하여 View와 양방향 연동하는 Controller에 대한 Unit Test 수행

    + Directives/Filter 의 내부 로직에 대한 Unit Test 수행 

    (Mocha 기반의 Chai와 같은 expectation, assertion 사용)


 1-4. 개발 툴체인

  - Yeoman 을 통하여 AngularJS 스케폴딩 코딩을 한다 (참조)

  - 1-3의 E2E 와 Unit Testing을 위하여 AngularJS의 Karma Test Framework안에서 사용하면 된다. 단, Protractor사용시 예외



2. 테스트 전략예 동영상 

  - AngularJS에 대한 Test 전략에 대한 동영상을 보자 

    + testem을 통해 Unit Test를 수행

    + protractor를 통해 E2E Test를 수행

    + 제품 통합 테스트 수행을 위하여 Yeoman과 유사한 Lineman을 사용

    + Jasmine-given을 사용하여 Given-When-Then의 CoffeeScript 방식의 재미있는 테스팅도 소개함

     


  - 새롭게 나온 E2E Test Framework인 Protractor for angularJS 

    + Angular팀에서 새롭게 소개된 Protractor 느낌상 Karma E2E 보다는 더 간단하고 직관적인듯 하다.

     



<참조>

  - End-to-End Testing with AngularJS

  - Front javascript testing framework

posted by 윤영식
2013. 10. 21. 19:01 Testing, TDD/Test First

Karma 테스트 프레임워크를 통하여 AngularJS기반 애플리케이션을 어떻게 테스트 하는지 알아보자 



AngularJS의 두가지 테스트 타입 

  - Unit Testing : 작고 격리된 코드 유닛의 동작에 대한 검증 테스트 

  - E2E Testing : 애플리케이션 전영역에 대한 테스트. E2E 테스터는 로봇처럼 브라우져를 실행하여 페이지를 검증해 준다 

  - AngularJS에서 이야기하는 E2E테스트 시나리오 API : Angular Scenario Runner 

    + describe 

    + beforeEach

    + afterEach

    + it

describe('Buzz Client', function() {

 it('should filter results', function() {

   input('user').enter('jacksparrow');

   element(':button').click();

   expect(repeater('ul li').count()).toEqual(10);

   input('filterText').enter('Bees');

   expect(repeater('ul li').count()).toEqual(1);

 });

});


  - Testing의 유형을 나누어 본다 : Unit, Midway, E2E

    + Unit Testing

      코드레벨 테스트

      격리된 테스트

      Mock & Stubbing 요구, 빠름

    + Midway Testing

      애플리케이션/코드레벨 테스트

      애플리케이션의 모든 부분 접근 가능

      다른 애플리케이션 코드와 상호작용

      뷰(html) 내부의 테스트는 불가능능

      빠름, 단, XHR일 경우 느릴 수 있음 

    + E2E Testing

      웹레벨 테스트

      웹서버가 필요

      완벽한 통합 테스트 



샘플코드 설치 및 수행

  - 데모 페이지

  - 소스 : GitHub Repository

  - 설치 (참조)

$ git clone git://github.com/yearofmoo-articles/AngularJS-Testing-Article.git

Cloning into 'AngularJS-Testing-Article'...

remote: Counting objects: 536, done.

remote: Compressing objects: 100% (368/368), done.

remote: Total 536 (delta 130), reused 516 (delta 117)

Receiving objects: 100% (536/536), 1.28 MiB | 390.00 KiB/s, done.

Resolving deltas: 100% (130/130), done.


$ cd AngularJS-Testing-Article/ && npm install


// 만일 제대로 npm install시 에러가 발생하면 package.json의 버전을 바꾼다

$ vi package.json

   "grunt-karma": "latest",

    "karma": "latest",

  - 수행 : http://localhost:8000 으로 호출한다 

$ grunt server

Running "connect:server" (connect) task

Starting connect web server on localhost:8000.

Waiting forever...

  - 디렉토리 구조 

    + test 밑에 3가지의 karma 환경파일이 존재한다

    + grunt test 명령으로 test 프로그램을 수행할 수 있다 - grunt test, grunt karma:unit, grunt karma:midway, grunt test:e2e (참조)

       * 테스트 안될 경우 주의

        1) bower_components를 components 폴더로 명칭 변경

        2) ngMidwayTester 못 찾으면 : comoponents/ngMidwayTest/src 에서 src를 Source 폴더로 명칭 변경  

    + 각 파트별 수행 테스트 파일은 unit, midway, e2e 폴더로 나뉜다 

    

  - Karma안에서 사용한 Test Frameworks은 test/ 폴더 밑으로 karma-e2e/midway/unit/shared.conf.js 로 설정되어 있다 

    + Mocha.js : Unit Test

    + Chai.js : matchers and assertions

    + Jasmine.js : End-To-End Test(E2E) Framework으로 Angular Scenario Runner에서 사용


  * Midway Plugin에서 에러가 발생한다. 해결하면 500원



Mocha, Chai 그리고 Angular Scenario Runner

  - Unit 과 Midway를 테스트에는 테스트 스팩은 Mocha(모카)를 이용하고 Matcher/Assertion으로 Chai를 사용한다  

  - E2E 테스트시에는 Mocah, Chai를 사용할 수 없었다. 별도의 E2E 수행 문서를 제공한다

  - 테스트 시에 AngularJS는 XHR 요청도 Interceptors를 통해서 가로챌 수 있다. 

     그외 route, controller, service, directive까지도 가로채서 테스트 코드를 넣을 수 있다. 

  - XHR 가로채기의 비용이 크면 angular-mock.js를 통하여 Mock을 사용할 수도 있다 



1. Testing Module

  - AngularJS에서 모듈은 directives, controllers, templates, services 그리고 resources 생성을 위한 컨테이너 오브젝트이다

  - 따라서 테스트에 최초로 할 것이 해당 모듈이 있는지 체크하는 것이다 

  - Midway 테스트에서 수행한다 

  - midway/appSpec.js

//
// test/midway/appSpec.js
//
describe("Midway: Testing Modules", function() {
  describe("App Module:", function() {

    var module;
    before(function() {
      module = angular.module("App");
    });

    it("should be registered", function() {
      expect(module).not.to.equal(null);
    });

    // App이 의존하는 모듈들이 있는지 검증 
    describe("Dependencies:", function() {

      var deps;
      var hasModule = function(m) {
        return deps.indexOf(m) >= 0;
      };
      before(function() {
        deps = module.value('appName').requires;
      });

      //you can also test the module's dependencies
      it("should have App.Controllers as a dependency", function() {
        expect(hasModule('App.Controllers')).to.equal(true);
      });

      it("should have App.Directives as a dependency", function() {
        expect(hasModule('App.Directives')).to.equal(true);
      });

      it("should have App.Filters as a dependency", function() {
        expect(hasModule('App.Filters')).to.equal(true);
      });

      it("should have App.Routes as a dependency", function() {
        expect(hasModule('App.Routes')).to.equal(true);
      });

      it("should have App.Services as a dependency", function() {
        expect(hasModule('App.Services')).to.equal(true);
      });
    });
  }); 

});



2. Testing Routes

  - 애플리케이션의 페이지 라우팅이 제대로 되는지 테스트 한다 

  - Midway, E2E 테스트에서 수행한다 

  - Midway Testing (Midway Testser Plugin)

//
// test/midway/routesSpec.js
//
describe("Testing Routes", function() {

  var test;

  before(function(done) {
    test = new ngMidwayTester();
    test.register('App', done);
  });

  it("should have a videos_path", function() {
    expect(ROUTER.routeDefined('videos_path')).to.equal(true);
    var url = ROUTER.routePath('videos_path');
    expect(url).to.equal('/videos');
  });

  it("the videos_path should goto the VideosCtrl controller", function() {
    var route = ROUTER.getRoute('videos_path');
    route.params.controller.should.equal('VideosCtrl');
  });

  it("the home_path should be the same as the videos_path", function() {
    expect(ROUTER.routeDefined('home_path')).to.equal(true);
    var url1 = ROUTER.routePath('home_path');
    var url2 = ROUTER.routePath('videos_path');
    expect(url1).to.equal(url2);
  });
 

});

  - E2E Testing 

// test/e2e/routesSpec.js
//
describe("E2E: Testing Routes", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  it('should jump to the /videos path when / is accessed', function() {
    browser().navigateTo('#/');
    expect(browser().location().path()).toBe("/videos");
  });

  it('should have a working /videos route', function() {
    browser().navigateTo('#/videos');
    expect(browser().location().path()).toBe("/videos");
  });

  it('should have a working /wathced-videos route', function() {
    browser().navigateTo('#/watched-videos');
    expect(browser().location().path()).toBe("/watched-videos");
  });

  it('should have a working /videos/ID route', function() {
    browser().navigateTo('#/videos/10');
    expect(browser().location().path()).toBe("/videos/10");
  });

});



3. Testing Controllers

  - $scope를 통하여 template 페이지와 데이터 바인딩 되는 테스트 

  - Unit Testing 

// test/unit/controllers/controllersSpec.js
//
describe("Unit: Testing Controllers", function() {

  beforeEach(angular.mock.module('App'));

  it('should have a VideosCtrl controller', function() {
    // controller 모듈이 있는지 검증
    expect(App.VideosCtrl).not.to.equal(null);
  });

  it('should have a VideoCtrl controller', function() {
    expect(App.VideoCtrl).not.to.equal(null);
  });

  it('should have a WatchedVideosCtrl controller', function() {
    expect(App.WatchedVideosCtrl).not.to.equal(null);
  });

  // $httpBackend Mock 객체를 통해 단위 테스트 검증
  it('should have a properly working VideosCtrl controller', 
    inject(function($rootScope, $controller, $httpBackend) {
    var searchTestAtr = 'cars';
    var response = $httpBackend.expectJSONP('https://gdata.youtube.com/feeds/api/videos?q='
                     + searchTestAtr + '&v=2&alt=json&callback=JSON_CALLBACK');
    response.respond(null);

    var $scope = $rootScope.$new();
    var ctrl = $controller('VideosCtrl', {
      $scope : $scope,
      $routeParams : {
        q : searchTestAtr
      }
    });
  }));

  it('should have a properly working VideoCtrl controller', 
       inject(function($rootScope, $controller, $httpBackend) {
    var searchID = 'cars';
    var response = $httpBackend.expectJSONP('https://gdata.youtube.com/feeds/api/videos/' 
                     + searchID + '?v=2&alt=json&callback=JSON_CALLBACK');
    response.respond(null);

    var $scope = $rootScope.$new();
    var ctrl = $controller('VideoCtrl', {
      $scope : $scope,
      $routeParams : {
        id : searchID
      }
    });
  }));

  it('should have a properly working WatchedVideosCtrl controller', 
         inject(function($rootScope, $controller, $httpBackend) {
    var $scope = $rootScope.$new();

    //we're stubbing the onReady event
    $scope.onReady = function() { };
    var ctrl = $controller('WatchedVideosCtrl', {
      $scope : $scope
    });
  }));
 

});

  - Midway Testing

// test/midway/controllers/controllersSpec.js

describe("Midway: Testing Controllers", function() {

  var test, onChange;
  // Async Test로 done 객체 파라미터 전달 
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      done();
    });
  });

  // 라투트통하여 페이지 변경 성공 이벤트 발생하면 onChange() 호출
  before(function() {
    test.$rootScope.$on('$routeChangeSuccess', function() {
      if(onChange) onChange(); 
    });
  });

  beforeEach(function(done) {
    test.reset(done);
  });

  it('should load the VideosCtrl controller properly when /videos route is accessed', function() {
    onChange = function() {
      test.path().should.eq('/videos');
      var current = test.route().current;
      var controller = current.controller;
      var scope = current.scope;
      expect(controller).to.equal('VideosCtrl');
    };
    test.path('/videos'); // /videos 호출되면 자동으로 onChange() 호출되어 검증 
  });

  it('should load the WatchedVideosCtrl controller properly when /watched-videos route is accessed', function(done) {
    onChange = function() {
      test.path().should.eq('/watched-videos');
      var current = test.route().current;
      var controller = current.controller;
      var params = current.params;
      var scope = current.scope;

      expect(controller).to.equal('WatchedVideosCtrl');
      done();
    };
    test.path('/watched-videos');
  });

});

  - E2E Testing

// test/e2e/controllers/controllersSpec.js
//
describe("E2E: Testing Controllers", function() {

  // 첫 페이지 있는지 검증 
  beforeEach(function() {
    browser().navigateTo('/');
  });

  // videos의 html 화면에서 DIV id="ng-view"의 html 내역 안에 data-app-youtube-listings이 있는지 검증   
  it('should have a working videos page controller that applies the videos to the scope', function() {
    browser().navigateTo('#/');
    expect(browser().location().path()).toBe("/videos");
    expect(element('#ng-view').html()).toContain('data-app-youtube-listings');
  });

  it('should have a working video page controller that applies the video to the scope', function() {
    browser().navigateTo('#/videos/WuiHuZq_cg4');
    expect(browser().location().path()).toBe("/videos/WuiHuZq_cg4");
    expect(element('#ng-view').html()).toContain('app-youtube-embed');
  }); 

}); 



4. Testing Services/Factories 

  - 서비스는 테스트가 가장 쉬운 코들 블럭이다 

  - Service와 Factory에 대해서 테스트 하는 방법

  - Unit Testing

// test/unit/services/servicesSpec.js
//
describe("Unit: Testing Controllers", function() {

  beforeEach(angular.mock.module('App'));

  it('should contain an $appStorage service', inject(function($appStorage) {
    expect($appStorage).not.to.equal(null);
  }));

  // 서비스 존재 유무 검증 
  it('should contain an $appYoutubeSearcher service', inject(function($appYoutubeSearcher) {
    expect($appYoutubeSearcher).not.to.equal(null);
  }));

  it('should have a working $appYoutubeSearcher service', inject(['$appYoutubeSearcher',function($yt) {
    expect($yt.prefixKey).not.to.equal(null);
    expect($yt.resize).not.to.equal(null);
    expect($yt.prepareImage).not.to.equal(null);
    expect($yt.getWatchedVideos).not.to.equal(null);
  }]));

  // $appYoutubeSearcher Factory 검증 
  it('should have a working service that resizes dimensions', inject(['$appYoutubeSearcher',function($yt) {
    var w = 100;
    var h = 100;
    var mw = 50;
    var mh = 50;
    var sizes = $yt.resize(w,h,mw,mh);
    expect(sizes.length).to.equal(2);
    expect(sizes[0]).to.equal(50);
    expect(sizes[1]).to.equal(50);
  }]));

  // $appStorage Factory 검증 
  it('should store and save data properly', inject(['$appStorage',function($storage) {
    var key = 'key', value = 'value';
    $storage.enableCaching();
    $storage.put(key, value);
    expect($storage.isPresent(key)).to.equal(true);
    expect($storage.get(key)).to.equal(value);

    $storage.erase(key);
    expect($storage.isPresent(key)).to.equal(false);

    $storage.put(key, value);
    $storage.flush();
    expect($storage.isPresent(key)).to.equal(false);
  }]));
 

});

  - Midway Testing 

// test/midway/services/servicesSpec.js
//
describe("Midway: Testing Services", function() {

  var test, onChange, $injector;

  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      done();
    });
  });

  // 화면 변경이 있을 경우 onChange() 호출 
  before(function() {
    test.$rootScope.$on('$routeChangeSuccess', function() {
      if(onChange) onChange(); 
    });
  });

  before(function() {
    $injector = test.injector();
  });

  beforeEach(function() {
    //test.reset();
  });

  // $appYoutubeSearcher 를 $injector로 부터 가져옴. findeVideo 호출함 
  it('should perform a JSONP operation to youtube and fetch data', function(done) {
    var $yt = $injector.get('$appYoutubeSearcher');
    expect($yt).not.to.equal(null);

    //this is the first video ever uploaded on youtube
    //so I doubt it will be removed anytime soon
    //and should be a good testing item
    var youtubeID = 'jNQXAC9IVRw';

    $yt.findVideo(youtubeID, false,
      function(q, data) {
        expect(data).not.to.equal(null);
        expect(data.id).to.equal(youtubeID);
        done();
      }
    );
  });
 

});



5. Testing Directives

  - AngularJS입장에서 HTML 코드안에서 컴포넌트 역할이다 

    HTML상에 반복되며 격리될 수 있는 것을 Directive로 만든다. 따라서 HTML 코드 내역은 최소화 되고 불필요한 반복을 줄일 수 있다.

  - Directive가 $scope와 DOM을 가지고 어떻게 작동할지 테스트 해야 한다 

  - XHR을 호출할 수도 있다. 따라서 Unit, Midway, E2E 전부분의 테스트를 수행한다 

  - Unit Testing 

// test/unit/directives/directivesSpec.js
//
describe("Unit: Testing Directives", function() {

  var $compile, $rootScope;

  beforeEach(angular.mock.module('App'));

  // 
  beforeEach(inject(
    ['$compile','$rootScope', function($c, $r) {
      $compile = $c;
      $rootScope = $r;
    }]
  ));

  // app-welcome Directive안에 Welcome 글자와 매치하는지 검증
  it("should display the welcome text properly", function() {
    var element = $compile('<div data-app-welcome>User</div>')($rootScope);
    expect(element.html()).to.match(/Welcome/i);
  })

});

  - Midway Testing

// test/midway/directives/directivesSpec.js
//
describe("Midway: Testing Directives", function() {

  var test, $injector;

  // $injector 얻어오기 
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      done();
    });
  });

  before(function() {
    $injector = test.$injector;
  });

  // $appYoutubeSearcher 통하여 응답을 받은후 app-youtube-listings Directive를 검증한다. XHR호출이므로 setTimeout 값으로 1초를 준다 
  it("should properly create the youtube listings with the directive in mind", function(done) {
    var $youtube = $injector.get('$appYoutubeSearcher');

    var html = '';
    html += '<div data-app-youtube-listings id="app-youtube-listings">';
    html += ' <div data-ng-include="\'templates/partials/youtube_listing_tpl.html\'" data-ng-repeat="video in videos"></div>';
    html += '</div>';

    var $scope = test.scope();
    var element = angular.element(html);

    $youtube.query('latest', false, function(q, videos) {
      $scope.videos = videos;

      test.directive(element, $scope, function(element) {
        setTimeout(function() {
          var klass = (element.attr('class') || '').toString();
          var hasClass = /app-youtube-listings/.exec(klass);
          expect(hasClass.length).to.equal(1);

          var kids = element.children('.app-youtube-listing');
          expect(kids.length > 0).to.equal(true);
          done();
        },1000);
      });
    });
  });

});

  - E2E Testing

// test/e2e/directives/directivesSpec.js
//
describe("E2E: Testing Directives", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  // 화면을 이동하였을 때 Directive가 표현하는 HTML 내역이 있는지 검증
  it('should have a working welcome directive apply it\'s logic to the page', function() {
    browser().navigateTo('#/videos');
    expect(browser().location().path()).toBe("/videos");
    expect(element('#app-welcome-text').html()).toContain('Welcome');
  });

  it('should have a working youtube listing directive that goes to the right page when clicked', function() {
    browser().navigateTo('#/videos');
    element('.app-youtube-listing').click();
    expect(browser().location().path()).toMatch(/\/videos\/.+/);
  });

});




6. Testing Templates, Partials & Views 

  - 격리된 HTML 코드이다 

  - 앵귤러에서는 ngView 또는 ngInlcude 통하여 렌더링된다 

  - 뷰는 $templateCache 서비스 통해서 캐싱되어 제공된다. 

    테스트시 templateUrl을 주면 캐싱된 mock template을 얻어와 사용할 수 있다 

  - Midway Testing

// test/midway/templates/templatesSpec.js
//
describe("Midway: Testing Requests", function() {

  var test, onChange;
  var $location, $injector, $params;

  // url path 변경시 onChange() 호출
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      test.$rootScope.$on('$routeChangeSuccess', function() {
        if(onChange) onChange(); 
      });
      done();
    });
  });

  before(function() {
    $injector = test.$injector;
    $location = test.$location;
    $params   = test.$routeParams;
  });

  beforeEach(function() {
    onChange = null;
  });

  // templateUrl을 test.path('XXX') 호출후 template 명칭이 맞는지 검증
  it("should load the template for the videos page properly", function(done) {
    onChange = function() {
      setTimeout(function() {
        var current = test.route().current;
        var controller = current.controller;
        var template = current.templateUrl;
        expect(template).to.equal('templates/views/videos/index_tpl.html');
        onChange = null
        done();
      },1000);
    };
    test.path('/videos?123');
  });
 

});

  - E2E Testing

// test/e2e/templates/templatesSpec.js
//
describe("E2E: Testing Templates", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  // 각 templateUrl 이동시 화면 내역을 검증
  it('should redirect and setup the videos page template on root', function() {
    browser().navigateTo('#/');
    expect(element('#ng-view').html()).toContain('youtube_listing');
  });

  it('should load the watched videos template into view', function() {
    browser().navigateTo('#/watched-videos');
    expect(element('#ng-view').html()).toContain('youtube_listing');
  });

  it('should load the watched video template into view', function() {
    browser().navigateTo('#/videos/123');
    expect(element('#ng-view').html()).toContain('profile');
  });

  it('should redirect back to the index page if anything fails', function() {
    browser().navigateTo('#/something/else');
    expect(element('#ng-view').html()).toContain('youtube_listing');
  });
 

});



7. Testing Filters

  - filter 테스트시에는 $injector 서비스가 필요하고, $injector로 부터 $filter를 얻어온다 

  - Unit Testing

// test/unit/filters/filtersSpec.js
//
describe("Unit: Testing Filters", function() {

  beforeEach(angular.mock.module('App'));

  it('should have a range filter', inject(function($filter) {
    expect($filter('range')).not.to.equal(null);
  }));

  // $filter 서비스를 받아서 range 필터를 얻는다 
  it('should have a range filter that produces an array of numbers', inject(function($filter) {
    var range = $filter('range')([], 5);
    expect(range.length).to.equal(5);
    expect(range[0]).to.equal(0);
    expect(range[1]).to.equal(1);
    expect(range[2]).to.equal(2);
    expect(range[3]).to.equal(3);
    expect(range[4]).to.equal(4);
  }));

  it('should return null when nothing is set', inject(function($filter) {
    var range = $filter('range')();
    expect(range).to.equal(null);
  }));

  // range filter의 input에 대한 검증
  it('should return the input when no number is set', inject(function($filter) {
    var range, input = [1];
    range = $filter('range')(input);
    expect(range).to.equal(input);

    range = $filter('range')(input, 0);
    expect(range).to.equal(input);

    range = $filter('range')(input, -1);
    expect(range).to.equal(input);

    range = $filter('range')(input, 'Abc');
    expect(range).to.equal(input);
  }));
 

});

  - Mideway Testing

// test/midway/filters/filtersSpec.js

//
describe("Midway: Testing Filters", function() {

  var test, onChange, $filter;

  // 최초 한번 미리 $filter 서비스를 얻어온다 
  before(function(done) {
    ngMidwayTester.register('App', function(instance) {
      test = instance;
      test.$rootScope.$on('$routeChangeSuccess', function() {
        if(onChange) onChange(); 
      });

      $filter = test.filter();

      done();
    });
  });

  beforeEach(function() {
    test.reset();
  });

  it('should have a working range filter', function() {
    expect($filter('range')).not.to.equal(null);
  });

  // html 템플릿 DOM을 만들어서 append 하고, Directives에 사용한 range에 대해서 검증
  it('should have a working filter that updates the DOM', function(done) {
    var id = 'temp-filter-test-element';
    var html = '<div id="' + id + '"><div class="repeated" ng-repeat="n in [] | range:10">...</div></div>';
    var element = angular.element(html);
    angular.element(document.body).append(html);

    test.directive(element, test.scope(), function(element) {
      var elm = element[0];
      setTimeout(function() {
        var kids = elm.getElementsByTagName('div');
        expect(kids.length).to.equal(10);
        done();
      },1000);
    });
  });

});

  - E2E Testing 

// test/e2e/filters/filtersSpec.js

//
describe("E2E: Testing Filters", function() {

  beforeEach(function() {
    browser().navigateTo('/');
  });

  // range 필터가 사용된 화면으로 이동하였을 경우 화면이 정상적으로 표시 되는지 검증
  it('should have a filter that expands the stars properly', function() {
    browser().navigateTo('#/videos/WuiHuZq_cg4');
    expect(repeater('#app-youtube-stars > .app-youtube-star').count()).toBeGreaterThan(0);
  });

});



<참조>

  - 원문 : Full Spectrum Test in AngularJS 

  - Javascript 테스트를 어렵게 하는 6가지

  - Service Test 전략 동영상

    


  - AngularJS를 위한 새로운 E2E Test Framework "Protractor"

  - Protractor로 E2E 테스트하기 : 기존 Karma E2E 보다 쉬워진 느낌이다. 30분 부터 보시라

    


posted by 윤영식
2013. 10. 20. 21:12 Testing, TDD/Test First

AngularJS 기반하에 TDD를 수행하거나 테스트 코드를 작성할 때 적합한 툴과 개념에 대해 이해를 하자



1. Unit Testing

  - 단위 기능 테스트 

  - 추천 단위 테스트 툴

    + Mocha : 단순하며 유연한 테스트 프레임워크 (추천)

    + Jasmine : Behavior-Driven Development Framework for testing Javascript (추천)

    + QUnit : jQuery에서 사용

  - 단위 테스트 만들기 

    + 사용자 스토리 기반으로 만들어 보자 

       "사용자로써 나의 앱에 로그인을 하면 대시보드 페이지를 본다"

    + Story -> Features -> Units 로 구분을 한다 

       "로그인 폼"을 하나의 기능으로 선택한다

       하나의 스토리는 여러 기능의 합집합이며, 각 기능의 정상작동 검증(Verification)을 거쳐서 하나의 스토리는 완성된다 

    + 로그인 폼에 대한 Spec을 만든다 (BDD)

describe('user login form', function() {


    // critical : 기능의 검증

    it('ensure invalid email addresses are caught', function() {});

    it('ensure valid email addresses pass validation', function() {});

    it('ensure submitting form changes path', function() { });


    // nice-to-haves

    it('ensure client-side helper shown for empty fields', function() { });

    it('ensure hitting enter on password field submits form', function() { });


});

   

  - 위의 테스트 Spec을 AngularJS로 단위 테스트 만들기 

    + 파일 : angular.js, angular-mocks.js, app.js(모듈명-App), loginController.js(LoginCtrl),  loginService.js(LoginService)

    + beforeEach에서 angular.mock.module 통해 모듈을 셋업한다 

       beforeEach에서 angular.mock.inject 통해 controller에서 $scope 셋업한다 (참조) 

    + it 에서 inject 통하여 service를 셋업한다 (참조)

describe("Unit Testing Examples", function() {


  beforeEach(angular.mock.module('App')); 


  it('should have a LoginCtrl controller', function() {

    expect(App.LoginCtrl).toBeDefined();

  });


  it('should have a working LoginService service', inject(['LoginService',

    function(LoginService) {

      expect(LoginService.isValidEmail).not.to.equal(null);


      // test cases - testing for success

      // 검증을 위한 테스트 데이터 

      var validEmails = [

        'test@test.com',

        'test@test.co.uk',

        'test734ltylytkliytkryety9ef@jb-fe.com'

      ];


      // test cases - testing for failure

      var invalidEmails = [

        'test@testcom',

        'test@ test.co.uk',

        'ghgf@fe.com.co.',

        'tes@t@test.com',

        ''

      ];


      // you can loop through arrays of test cases like this

      for (var i in validEmails) {

        var valid = LoginService.isValidEmail(validEmails[i]);

        expect(valid).toBeTruthy();

      }

      for (var i in invalidEmails) {

        var valid = LoginService.isValidEmail(invalidEmails[i]);

        expect(valid).toBeFalsy();

      }


    }]) // end inject 

  ); // end it 

}); // end describe



2. Integration Testing 

  - 통합 테스트는 end-to-end 테스트(E2E)이고, 수행순서대로 차례로 수행해 보는 것이다. 

     그리고 애플리케이션과 UI의 상태를 검증한다. 심지어 Visual적인 부분에 대한 검증도 가능하다 

  - 추천 도구 

    + Karma : AngularJS에서 공식사용 (추천)

    + CasperJS : headless browser로 사용 

    + PhantomJS : headles brower

  - 통합 테스트 만들기 

    + "사용자로써 나의 앱에 로그인하여 대시보드 페이지를 본다"

describe('user login', function() {


  // 수행 및 결과 상태의 검증

  it('ensures user can log in', function() {

    // expect current scope to contain username

  });

  it('ensures path has changed', function() {

    // expect path to equal '/dashboard'

  });


});


  - 위의 테스트 Spec을 AngularJS로 통합 테스트 만들기 (참조)

    + 최근에는 Angular Scenario Test Runner와 같은 Protractor E2E test framework이 새롭게 소개되고 있다 

describe("Integration/E2E Testing", function() {


  // start at root before every test is run

  beforeEach(function() {

    browser().navigateTo('/');

  });


  // test default route

  it('should jump to the /home path when / is accessed', function() {

   // 루트 패스로 이동 - 로그인 화면 

    browser().navigateTo('#/');

    expect(browser().location().path()).toBe("/login");

  });


  it('ensures user can log in', function() {

    // 로그인 화면인지 체크

    browser().navigateTo('#/login');

    expect(browser().location().path()).toBe("/login");


    // assuming inputs have ng-model specified, and this conbination will successfully login

    // 테스트 데이터를 통해 브라우져 로그인 submit 버튼 click 이벤트 발생시킴 

    input('email').enter('test@test.com');

    input('password').enter('password');

    element('submit').click();


    // logged in route

    // 로그인이 성공했을 경우 대시보드 패스에 있는지 검증

    expect(browser().location().path()).toBe("/dashboard");


    // my dashboard page has a label for the email address of the logged in user

    // DIV의 id="email"의 html 값이 로그인시 입력한 값인지 검증 

    expect(element('#email').html()).toContain('test@test.com');

  });


  it('should keep invalid logins on this page', function() {

    // 로그인 화면 인지 검증

    browser().navigateTo('#/login');

    expect(browser().location().path()).toBe("/login");


    // assuming inputs have ng-model specified, and this conbination will successfully login

    // 잘 못된 값을 넣었을 가지고 로그인 버튼 클릭 

    input('email').enter('invalid@test.com');

    input('password').enter('wrong password');

    element('submit').click();


    // DIV id="message"에 failed메세지 있는지 검증 

    expect(element('#message').html().toLowerCase()).toContain('failed');


    // logged out route

    // 제대로 로그인을 못했으므로 로그인 패스 그대로인지 검증 

    expect(browser().location().path()).toBe("/login");

  }); // end it 

}); // end describe 



3. Mocking Services and Modules in AngularJS

  - 목(Mock) 서비스를 이용한 테스트 방법 

  - 목 오브젝트를 사용하는 이유는 외부적인 요소 의존적인 부분을 제거하고 테스트 하기 위해서 이다 

  - ngMock.$httpBackend : service에서 http login 호출을 하면 응답 JSON 객체를 넘겨주는 mock 객체를 만든다 

it('should get login success',

  inject(function(LoginService, $httpBackend) {


    $httpBackend.expect('POST', 'https://api.mydomain.com/login')

      .respond(200, "[{ success : 'true', id : 123 }]");


    LoginService.login('test@test.com', 'password')

      .then(function(data) {

        expect(data.success).toBeTruthy();

    });


  $httpBackend.flush();

});



<참조>

  - 원문 : Best Practices in AngularJS

  - AngularJS Controller Unit Test 방법

  - AngularJS Service Unit Test 방법

  - Full Spectrum Testing(E2E) in Angular with Karma (필독)

  - Introducing E2E Testing in Angular 동영상

    

posted by 윤영식
2013. 10. 10. 11:19 MongoDB/Concept

MongoDB가 나오게 된 이유와 개념/구성에 대하여 알아보자



어느 방향을 선택할 것인가

  - 500만 : trend

  - 1000만 : culture

  - 2000만 : 안끼면 소외된다 

  소셜네트워크가 IT와 만나고 다시 스마트폰을 만나면서 새로운 방향을 제시하였다. 이런 방향을 지원하기 위하여 많은 OSS(Open Source Software)가 나왔고, HTTP 통신(Stateless)을 통하여 유입되는 어마어마한 사용자를 수용하기(규모의 확장) 위하여 사람들의 생각을 저장하고 활용해야 하는 필요성이 커졌다. 사람의 생각을 Key=Value로 많은 데이터를(BigData) 저장할 수 있어야 한다. 이러한 사상은 예전의 Client-Server 사상이 아닌 HTTP Web상의 통합과 유입을 처리하는 SNS(Social Network Service), SND(Social Network Data), SNG(Social Network Game) 사상으로 가고 있다. SNS는 HTTP 프로토콜을 통하여 Big Data Stream을 만들어 Real Time으로 반응하는 것이다. 



기술의 흐름에 따른 선택

1) 전통적 웹기술

  - 전통 pc application 개발 기술

  - static 기술

  - dynamic 기술


3) 모바일 기술

  - embedded 기술 (고립된 기술) : iphone, android 등 mobile application 개발 기술 또는 Sencha 기술 

  - open api 기술 : web services 기술 


4) 웹앱(WebApp) 기술 : 3세대 웹

  - SPA(스타) : single page application 기술 (javascript, json)

  - MVC framework을 client 사이트로 내림 = Fluent MVC = Stream

    (스트림은 융합을 쉽게 이루어지게 한다. 예) 디지털TV, 무인자동차) 

    + client Controller는 없다. View가 Controller를 대신한다 

    + MOV : Model + Operation + View가 있다. 여기서 Operation = Functional 이 된다. (Functional Language, Async) 

    + DRY : Don't Repeatly Yourself 반복하지 않는 프레임워크    

    



  - SNS : Modern Web = Smart Device (TV, Phone등), 반대개념 일반 Web 임

  - Real Time 기술 : websocket 기술이 들어감 

  - Javascript가 소통 언어가 되고 JSON이 데이터 포멧이 되어 end-to-end에서 커뮤니케이션의 실체가 된다. 

    + 기존 ORM-Object Relational Mapping- 기술은 사용하지 말자~~

    + client : 메모리, 브라우져 사용

    + server : 모듈 서비스. 일반 모듈의 대표는 Node.js 이고, special module = DB module    

  - Stream의 통합 : Edge 통합

    + Facebook이 초창기 사용하였으나 지금은 웹앱, mongodb를 사용하지 않는다 (초기 기술 적용에 대한 실패일까?)


  - 최종 모습

    + client : Backbone, Handlebar(mustache)

    + server : Node.js, Redis(맵데이터=view테이블), MongoDB(MapReducing, NoSQL)

      MapReduce를 통하면 BI(Business Intelegence)를 구현한다

    + bigdata : 큰단위 - Hadoop + HBase,  작은단위 - Redis + MongoDB로 보아도 된다

    + Agile 개발 : Schemaless 로 에자일 개발 프로세스에 적합하다. 작은 변화에도 기만하게 반응할 수 있다

     


MongoDB 이해하기 

  - 기존 RDB

    + join = projection 

    + 성능 optimization(최적화)를 위하여 schema와 join이 중요함 : 정규화의 필요(normalization)   

    

 

  - 현재 상황 

    + 성능 최적화를 위하여 찢어 놓았던(normalization)것을 고객 서비스를 위하여 다시 합쳐야 한다

    + SQL문이 8Kbytes 가 넘어 가고 현재 무한히 SQL을 쓸 수 있지만 늪이 된다. (튜닝을 위하여 컨설턴트가 필요해짐)

   - MongoDB

    + Table = Collection

    + Tuple = Document    

    

    


MongoDB 구성

  - Replica Set을 기본으로 가져가자 : HTTP를 통하여 묶는다 (Primary-Secondary)

  - Fail-Over를 통하여 High Availability(HA)를 충족한다 

    

  

  - Scale-Out 확장을 통하여 무한히 대응할 수 있다

    + Sharding을 통하여 데이터를 분산하여 처리하고 그안에 Replica-Set이 구성된다 

    + mongos는 router 이다     

    


  - 전체 MongoDB 구조 

    + Config Server : Replica-set, Shard 구성

    + Replica-set : Fail-Over (primary, secondary)

    + Sharding : Data Scale-out 

    

  


SNS는 이제 RealTime Stream Data 시대

  - 하나로 보이지만 여러곳에서 데이터가 통합되어 온다. 

    하기 그림에서 영상 화면 하나가 여러 곳에서 데이터가 모여져서 보여지듯이.

  - mongoDB의 GridFS가 된다 

  

   


MongoDB 개념 및 용어 정리 


MongoDB는 Service로 접근해야 한다! (애플리케이션 접근 옳지 않아~~)



<참조>

  - .Net Framework Frontier Facebook Group

  - KTH  MongoDB 어떻게 사용할 것인가?

posted by 윤영식
2013. 10. 8. 09:06 Lean Agile Culture/Lean Startup

장강일님 페이스북 글 발췌 


"더 나은 의사결정을 내리는 7단계"

1. Set a deadline.

의사결정의 반대는 지지부진 의사결정을 미루는 것이다. 우유부단함(indecision)은 다른 사람들이 당신을 대신해 의사결정내릴 때까지 망설이게 만든다. 그러므로, 중요한 의사결정은 마감일(deadline)이 있어야 한다. 당신이 "자율적으로(will)"으로 결정을 할 수 있는 최종기한을 뜻한다.

2. Discover the knowns.

최선의 판단을 하기 위해선, 의사결정에 필요한 정보들을 획득해야한다. 그런데, 정보를 취합할 때, 수확체감(diminishing returns)의 법칙이 적용된다는 것을 상기하라. 너무 세세한 디테일들에 과도하게 사로잡히기 시작할 때, 정보 수집을 멈추는 것이 좋다.

3. Gather relevant inputs.

의사 결정 이후에, 다른 사람들이 그 결정된 내용을 실행해야 한다면, "반드시" 그 사람들의 관점을 의사결정 내리기 전에 반영하라. 이 과정을 빠뜨리게 되면, 그 사람들은 당신이 내린 의사결정을 자신들의 것으로 수용(own)하지 않을 것이다. 당연히 매끄럽게 실행되기도 힘들다.

4. Decide.

결정하다(decide)의 어원은 라틴 말로 절단(cut off)에서 나왔다. 결정은 논쟁을 중단시키는 것이며, 다른 결정을 내릴 가능성들을 쳐내는 것이다. 일단 결정을 내리면, 바로 실행으로 나아가라. 그렇지 않으면, 당신이 내린건 진짜 의사결정이 아니다.

5. Explain your reasoning.

(3단계에서) 다른 사람들이 자신들의 관점을 피력할 기회를 주었다면, 이제 그들에게 왜 다른 옵션을 취하지 않고 이러한 의사결정을 내렸는지 설명하라. 그들은 기꺼이 당신의 의사결정에 따를 준비가 되어 있을 것이다.

6. Never second guess.

일단 의사결정을 내린 뒤에는 그 결정에 의구심을 갖지도 말고, 회의적인 시각을 지닌 사람들에게 심각하게 귀 기울이지 마라. 실제 실행에 온 힘을 기울이고, 결과를 얻기 전까지는 말이다. 이미 내려진 판단에 의심을 가질수록 우유부단함에 물들게 되며, 자연히 실행력도 떨어지게 된다.

7. Observe the results

정말로 진실되게 그리고 갖은 힘을 다해 실행에 주력했다면, 이제 한 걸음 뒤로 물러나 그 결과를 엄정하게 살펴보라. 기대하는 성과를 거두었다면 축하하라. 만약 그렇지 않다면, 8단계로 넘어가라.

8. Adjust the decision

명심하라!!! 꽉 막힌 완고함(Bullheadedness)은 우유부단함 못지 않게 당신을 대책없는 사람으로 전락시킨다. 오히려, 더 많은 자원과 노력을 낭비하게 만들면서 말이다. 그러므로, 기대하는 결과를 얻지 못했다면, 1단계로 다시 돌아가라. 그간 실행하면서 터득한 경험을 활용해 더 다듬고, 더 나은 의사결정을 내려라.



<참조>

  - 장강일 페이스북

posted by 윤영식