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

Publication

Statistics Graph

Recent Comment

2014.06.03 15:23 AngularJS/Start MEAN Stack

Angular Team에서 End-To-End 테스트를 위하여 별도로 개발한 프레임워크가 Protractor 이다. protractor는 WebDriverJs기반으로 만들어 졌다. 





Protractor 설정

  - Chrome 최신 버전 사용

  - protractor 설치 및 Chrome에서 selenium standalone server로 구동하기 위한 driver도 설치한다 

$ npm install -g protractor

$ webdriver-manager update 


  - selenium server를 기동하는 방법 : 4444 port로 listen한다 ( 호출 :  http://localhost:4444/wd/hub )

$ webdriver-manager start

seleniumProcess.pid: 55938

6월 02, 2014 5:08:16 오후 org.openqa.grid.selenium.GridLauncher main

정보: Launching a standalone server

Setting system property webdriver.chrome.driver to /usr/local/lib/node_modules/protractor/selenium/chromedriver

17:08:16.913 INFO - Java: Oracle Corporation 24.51-b03

17:08:16.915 INFO - OS: Mac OS X 10.9.3 x86_64

17:08:16.935 INFO - v2.41.0, with Core v2.41.0. Built from revision 3192d8a

17:08:17.079 INFO - Default driver org.openqa.selenium.ie.InternetExplorerDriver registration is skipped: registration capabilities Capabilities [{platform=WINDOWS, ensureCleanSession=true, browserName=internet explorer, version=}] does not match with current platform: MAC

17:08:17.145 INFO - RemoteWebDriver instances should connect to: http://127.0.0.1:4444/wd/hub

17:08:17.146 INFO - Version Jetty/5.1.x

17:08:17.148 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver]

17:08:17.149 INFO - Started HttpContext[/selenium-server,/selenium-server]

17:08:17.149 INFO - Started HttpContext[/,/]

17:08:17.510 INFO - Started org.openqa.jetty.jetty.servlet.ServletHandler@4b8e282c

17:08:17.510 INFO - Started HttpContext[/wd,/wd]

17:08:17.517 INFO - Started SocketListener on 0.0.0.0:4444

17:08:17.517 INFO - Started org.openqa.jetty.jetty.Server@3e578b06


  - 잘 동작하지 않는다면 Path에 JAVA_HOME을 설정한다. 



Protractor 환경 설정

  - 루트에 protractor-e2e.conf.js 파일 생성

  - 설정 : baseUrl은 grunt serve 시에 기동되는 port 번호

exports.config = {

  seleniumAddress: 'http://localhost:4444/wd/hub',

  capabilities: {'browserName': 'chrome'},

  specs: ['test/client/e2e/**/*.js'],

  baseUrl: 'http://localhost:9000',

  jasmineNodeOpts: {

    onComplete: null,

    isVerbose: true,

    showColors: true,

    includeStackTrace: true,

    defaultTimeoutInterval: 10000

  },

  framework: 'jasmine'

};

  - 체크

$ webdriver-manager start 

$ protractor protractor-e2e.conf.js

Using the selenium server at http://localhost:4444/wd/hub

[launcher] Running 1 instances of WebDriver


Finished in 0.001 seconds

0 tests, 0 assertions, 0 failures


[launcher] chrome passed

  - grunt를 이용하여 테스트 할 수 있도록 Gruntfile.js 에 설정. run: {..} 안의 options에 configFile 설정이 중요함 

$ npm install grunt-protractor-runner --save-dev


// grunt.initConfig 안에 설정

  protractor: {

      options: {

        keepAlive: true

      },

      run: {

        options: {

          configFile: "protractor-e2e.conf.js", // Target-specific config file

          args: {} // Target-specific arguments

        }

      }

  },


// task 설정

  grunt.registerTask('test', function(target) {

    if (target === 'server') {

      return grunt.task.run([

        'env:test',

        'mochaTest'

      ]);

    }


    else if (target === 'client') {

      return grunt.task.run([

        'concurrent:test',

        'autoprefixer',

        'karma',

        'protractor'

      ]);

    }


    else grunt.task.run([

      'test:server',

      'test:client'

    ]);

  });


// grunt test 에 첨부 

$ grunt test 또는 grunt test:client 수행 (단, 수행전에 webdriver-manager start 수행해 놓은 상태이어야함)


Running "protractor:run" (protractor) task

Using the selenium server at http://localhost:4444/wd/hub

[launcher] Running 1 instances of WebDriver


Finished in 0 seconds

0 tests, 0 assertions, 0 failures

[launcher] chrome passed

Done, without errors.


Execution Time (2014-06-02 15:43:51 UTC)

protractor:run  2.2s  ▇▇ 100%

Total 2.3s

  - 또는, grunt 설정을 하지 않았을 경우는 다음과 같이도 테스트 가능하다. 하지만 가급적 grunt를 이용하자.  

$ webdriver-manager start

$ protractor protractor-e2e.conf.js



End-to-End 테스트 코드 작성방식

  - 테스트 코드를 작성하기 위한 몇가지 개념을 익힌다. (위키)

> browser : webdriver를 감싼 객체. navigation 과 page information 을 갖음

> element : html element를 찾고 상호작용을 위한 helper method 

> by : element locator collection

> protractor : webdriver namespace를 wrapping한 protractor namespace


  - 샘플 코드 

    + browser.get : page loading 

    + element : page에서 element를 찾아줌 

    + by 종류 (소스 참조)

  • by.binding searches for elements by matching binding names, either from ng-bind or {{}} notation in the template.
  • by.model searches for elements by input ng-model.
  • by.repeater searches for ng-repeat elements. For example, by.repeater('phone in phones').row(11).column('price')returns the element in the 12th row (0-based) of the ng-repeat = "phone in phones" repeater with the binding matching{{phone.price}}.

describe('angularjs homepage', function() { it('should greet the named user', function() { // Load the AngularJS homepage. browser.get('http://www.angularjs.org'); // Find the element with ng-model matching 'yourName' - this will // find the <input type="text" ng-model="yourName"/> element - and then // type 'Julie' into it. element(by.model('yourName')).sendKeys('Julie'); // Find the element with binding matching 'yourName' - this will // find the <h1>Hello {{yourName}}!</h1> element. var greeting = element(by.binding('yourName')); // Assert that the text element has the expected value. // Protractor patches 'expect' to understand promises. expect(greeting.getText()).toEqual('Hello Julie!'); }); });


실제 End-to-End 테스트 코드

  - 로그인의 경우 e2e 코드 작성예 (필수참조)

'use strict';


var LoginPage = function() {

this.email = element(by.model('user.email'));

this.password = element(by.model('user.password'));


this.setEmail = function(email) {

this.email.sendKeys(email);

};


this.setPassword=  function(password) {

this.password.sendKeys(password);

};


this.url = function() {

browser.get('/#/signin');

};


this.login = function() {

return element(by.buttonText('button')).click();

};

};


describe('Login Page', function() {

it('should successfully login', function(){

var loginPage = new LoginPage();

loginPage.url();

loginPage.setEmail('test@test.com');

loginPage.setPassword('test');

loginPage.login().then(function(response){

console.log(response);

}, function(error){


})

});

});


  - 수행 : grunt test:client 

    + 수행 에러 발생함 : to be continue...



<참조>

  - Protractor 테스트 방법

  - Protractor Test PPT (필독)

  - Unit, E2E ToDo 예제

  - Protractor Grunt 설정하기

  - End-To-End Test 하기 예제

  - grunt-protractor-runner 

  - Angular Test Pattern

저작자 표시 비영리 변경 금지
신고
posted by peter yun 윤영식
2014.06.02 10:09 AngularJS/Start MEAN Stack

프로트앤드 개발을 하면서 테스트는 어떻게 할 수 있는지 알아본다. 앵귤러에서는 테스트 관련 내용까지도 포함하고 있어서 개발 테스트의 편의성을 함께 제공하고 있다. 


사전 준비

  generator-angular를 설치하게 되면 앵귤러 프레임워크에 카르마(Karma) 테스트 프레임워크와 Mock 객체등이 포함되어 있다. 과거에는 Karma를 통해서 단위(Unit) 테스트와 E2E (End-To-End) 테스트 두가지를 하였으나, 최근에는 Karma로는 단위 테스트만을 수행하고 Protractor라는 새로운 테스트 프레임워크를 통해서 E2E 테스트를 수행한다 


  - karma.conf.js : Unit Test 환경파일. 내부적으로 Jasmine (http://pivotal.github.io/jasmine/) 프레임워크를 사용한다

  - karma-e2e.conf.js : E2E Test 환경파일 (

  - "grunt test" 를 통해 수행


Karma가 Jasmine 테스트 프레임워크를 사용하고, 필요한 files 목록을 정의하며 Chrome 브라우져에서 8080 포트를 사용하여 테스트함. angular.js 와 angular-mocks.js 파일은 필 첨부 (angular-mocks.js 안에 module, inject 펑션 존재)

// karma.conf.js

// Karma configuration

// http://karma-runner.github.io/0.10/config/configuration-file.html


module.exports = function(config) {

  config.set({

    // base path, that will be used to resolve files and exclude

    basePath: '',


    // testing framework to use (jasmine/mocha/qunit/...)

    frameworks: ['jasmine'],


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

    files: [

      'app/bower_components/angular/angular.js',

      'app/bower_components/angular-mocks/angular-mocks.js',

      'app/bower_components/angular-resource/angular-resource.js',

      'app/bower_components/angular-cookies/angular-cookies.js',

      'app/bower_components/angular-sanitize/angular-sanitize.js',

      'app/bower_components/angular-route/angular-route.js',

      'app/scripts/*.js',

      'app/scripts/**/*.js',

      'test/mock/**/*.js',

      'test/spec/**/*.js'

    ],


    // list of files / patterns to exclude

    exclude: [],


    // web server port

    port: 8080,


    // level of logging

    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG

    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes

    autoWatch: false,

    // Start these browsers, currently available:

    // - Chrome

    // - ChromeCanary

    // - Firefox

    // - Opera

    // - Safari (only Mac)

    // - PhantomJS

    // - IE (only Windows)

    browsers: ['Chrome'],


    // Continuous Integration mode

    // if true, it capture browsers, run tests and exit

    singleRun: false

  });

};


  Gruntfile.js 의 test Task 내역 : grunt test 

// Test settings

karma: {

   unit: {

      configFile: 'karma.conf.js',

      singleRun: true

  }

}


... 중략 ..

grunt.registerTask('test', [

    'clean:server',

    'concurrent:test',

    'autoprefixer',

    'connect:test',

    'karma'

  ]);



1. Jasmine 테스트 프레임워크 

  describe ~ it 으로 BDD를 표현한다. BDD는 TDD의 2세대 버전 정도로 이해하자. 보다 자세한 사항은 위키 참조 (http://en.wikipedia.org/wiki/Behavior-driven_development

 describe("A suite", function() {

  it("contains spec with an expectation", function() {

    expect(true).toBe(true);

  });

});


 describe로 시작하는 것을 Suite (슈트)라고 부르고, it 으로 시작하는 것을 spec (스펙)이라고 부른다. 슈트안에 여러개의 스펙이 있을 수 있다. beforeEach(), afterEach()를 통해서 스펙 수행시 마다 환경 초기값 셋업(Setup)이나 마무리 작업(TearDown)을 수행할 수 있다. 스펙 안에는 expect 기대값에 대한 다양한 메서드를 제공한다.

toEqual()

toBe(), not.toBe()

toBeTruthy(), toBeFalsy()

toContain(), not.toContain()

toBeDefined(), toBeUndefined()

toBeNull()

toBeNaN()

toBeGreaterThan()

toBeLessThan()

toBeCloseTo() : 소수점

toMatch() : 정규표현식

toThrow()

 ...


  보다 자세한 사항은 Jasmine 문서를 참조한다 (http://jasmine.github.io/2.0/introduction.html)



2. 앵귤러 Service 테스트 준비

  yo를 통하여 앵귤러 코드를 완성하게 되면 자동으로 테스트 코드까지 생성된다. SessionService에 대한 테스트 코드는 /test/spec/services 폴더 밑에 동일한 session-service.js 파일이 존재한다. module 호출 후에 inject 가 올 수 있으나 inject 후에 module 호출은 올 수 없다. _ (underscore)를 양쪽에 붙이는 것은 angular프레임워크에 테스트를 위하여 해당 서비스를 사용함을 알려준다. 

describe('Service: SessionService', function () {

  // load the service's module

  beforeEach(module('meanstackApp'));


  // instantiate service

  var SessionService;

  beforeEach(inject(function (_SessionService_) {

    SessionService = _SessionService_;

  }));


  it('should do something', function () {

    expect(!!SessionService).toBe(true);

  });


});

 

 grunt test 명령을 수행하면 에러가 발생할 것이다. 그것은 index.html에 필요에 의해 추가한 .js파일을 Karma 환경파일에 추가하지 않았기 때문이다. controller 쪽에서도 에러가 발생하는데 기본 테스트 코드에서 발생하므로 불필요한 코드를 삭제한다. 

 // karma.conf.js 

  files: [

      'app/bower_components/jquery/jquery.js',

      'app/bower_components/bootstrap/dist/js/bootstrap.js',

      'app/bower_components/angular/angular.js',

      'app/bower_components/angular-mocks/angular-mocks.js',

      'app/bower_components/angular-resource/angular-resource.js',

      'app/bower_components/angular-cookies/angular-cookies.js',

      'app/bower_components/angular-sanitize/angular-sanitize.js',

      'app/bower_components/angular-route/angular-route.js',

      'app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',

      'app/bower_components/angular-ui-router/release/angular-ui-router.js',

      'app/scripts/*.js',

      'app/scripts/**/*.js',

      'test/mock/**/*.js',

      'test/spec/**/*.js'

    ],


  singleRun: true


// Controller 테스트 파일에서 불필요한 하기 코드 삭제 

  it('should attach a list of awesomeThings to the scope', function () {

    expect(scope.awesomeThings.length).toBe(3);

  });



3. 앵귤러 Service 테스트를 위한 Mock 사용

  SessionService 테스트 수행전에 공통적으로 사용하고 있는 Factory나 Service에 대한 테스트 코드를 작성한 후 SessionService를 최종 테스트 한다  


  - msRequestFactory : 원하는 JSON 객체가 생성되는지 테스트 

  - msRestfulApi : $httpBackend 이용하여 요청 처리 테스트

  - SessionService : $httpBackend 이용하여 요청 처리하고 SessionInfo의 정보 유무 테스트 


msRequestFactory 통하여 만들어진 JSON 객체가 원하는 key:value 값을 가지고 있는지 확인한다. expect().toEqual()을 사용한다 

 'use strict';


describe('Service: msRequestFactory', function () {


  // load the service's module

  beforeEach(module('meanstackApp'));


  // instantiate service

  var msRequestFactory;

  beforeEach(inject(function ($injector) {

    msRequestFactory = $injector.get('msRequestFactory');

  }));


  it('should exist', function () {

    expect(!!msRequestFactory).toBe(true);

  });


  it('should make JSON', function () {

    var request = msRequestFactory.createRequest('user', 'login', '');

    expect(request).toEqual({

      'area' : 'user',

      'resource' : 'login',

      'id' : '',

      'request' : {}

    });

  });


});

  

msRestfulApi는 $resource를 통해 서버에 요청을 보낸다. 서버 요청에 대한 Mock을 만들어서 응답을 줄 수 있는 기능으로 앵귤러는 $httpBackend를 제공한다. 즉, 실서버가 없을 경우 XHR 또는 JSONP 요청없이도 실서버에 요청한 것과 같은 응답을 줄 수 있다. $httpBackend의 유형은 두가지 이다


  - request expectation : 요청 JSON 데이터의 검증 원하는 결과가 왔는지 설정. expect().respond()

  - backend definition : 가상의 응답정보를  설정. when().respond()


서버의 요청은 모두 async 처리가 되므로 테스트코드 작성하기가 애매하다. 따라서 $httpBackend.flush()를 호출하는 시점에 request가 전달되므로, 테스트 코드 작성시 적절한 위치에 놓아야 한다. API 참조 (http://docs.angularjs.org/api/ngMock.$httpBackend)

// 로그인 서비스 테스트에서 계속 에러가 발생하여 Re-Thinking 중이다. 

// 에러 코드 

'use strict';


describe('Service: msRestfulApi', function () {


  // load the service's module

  beforeEach(module('meanstackApp'));


  // instantiate service

  var msRestfulApi, httpBackend, scope, resource;

  beforeEach(inject(function ($injector) {

    msRestfulApi = $injector.get('msRestfulApi');

    httpBackend = $injector.get('$httpBackend');

    scope = $injector.get('$rootScope');

    resource = $injector.get('$resource');


    httpBackend.when('POST', '/api/v1/user/login').respond(200, {'id': 1});

    

  }));


  it('should call test', function() {

    var request = {

      'area' : 'user',

      'resource' : 'login',

      'id' : '',

      'request' : {}

    };


    msRestfulApi.login(request, function(response){

      expect(response.id).toBe(1);

      console.log(response);

    }, function(error){

      console.log(error);

    });


    httpBackend.flush();

    expect(r.id).toBe(1);


  });


});

  


<참조>

  - Jasmine 문법

  - Jasmine 소개

  - Karma 테스트 하기

  - Protractor Seed 소개 : Protractor WebDriver 업데이트 

  - WebDriverJS 소개 

저작자 표시 비영리 변경 금지
신고
posted by peter yun 윤영식
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 peter yun 윤영식
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 peter yun 윤영식
prev 1 next