- 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(){varmodule;before(function(){module=angular.module("App");});it("should be registered",function(){expect(module).not.to.equal(null);});
// App이 의존하는 모듈들이 있는지 검증
describe("Dependencies:",function(){vardeps;varhasModule=function(m){returndeps.indexOf(m)>=0;};before(function(){deps=module.value('appName').requires;});//you can also test the module's dependenciesit("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);});});});
//// test/midway/routesSpec.js//describe("Testing Routes",function(){vartest;before(function(done){test=newngMidwayTester();test.register('App',done);});it("should have a videos_path",function(){expect(ROUTER.routeDefined('videos_path')).to.equal(true);varurl=ROUTER.routePath('videos_path');expect(url).to.equal('/videos');});it("the videos_path should goto the VideosCtrl controller",function(){varroute=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);varurl1=ROUTER.routePath('home_path');varurl2=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){
response.respond(null);var$scope=$rootScope.$new();varctrl=$controller('VideosCtrl',{$scope:$scope,$routeParams:{q:searchTestAtr}});}));it('should have a properly working VideoCtrl controller', inject(function($rootScope,$controller,$httpBackend){varsearchID='cars';varresponse=$httpBackend.expectJSONP('https://gdata.youtube.com/feeds/api/videos/' +searchID+'?v=2&alt=json&callback=JSON_CALLBACK');response.respond(null);var$scope=$rootScope.$new();varctrl=$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(){};varctrl=$controller('WatchedVideosCtrl',{$scope:$scope});}));
// 라투트통하여 페이지 변경 성공 이벤트 발생하면 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');varcurrent=test.route().current;varcontroller=current.controller;varscope=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');varcurrent=test.route().current;varcontroller=current.controller;varparams=current.params;varscope=current.scope;expect(controller).to.equal('WatchedVideosCtrl');done();};test.path('/watched-videos');});});
// 첫 페이지 있는지 검증
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){varw=100;varh=100;varmw=50;varmh=50;varsizes=$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){varkey='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);}]));
// 화면 변경이 있을 경우 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 itemvaryoutubeID='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 전부분의 테스트를 수행한다
// 화면을 이동하였을 때 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을 얻어와 사용할 수 있다
// templateUrl을 test.path('XXX') 호출후 template 명칭이 맞는지 검증it("should load the template for the videos page properly",function(done){onChange=function(){setTimeout(function(){varcurrent=test.route().current;varcontroller=current.controller;vartemplate=current.templateUrl;expect(template).to.equal('templates/views/videos/index_tpl.html');onChange=nulldone();},1000);};test.path('/videos?123');});
// 각 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){varrange=$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){varrange=$filter('range')();expect(range).to.equal(null);}));
// range filter의 input에 대한 검증it('should return the input when no number is set',inject(function($filter){varrange,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);}));
// 최초 한번 미리 $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){varid='temp-filter-test-element';varhtml='<div id="'+id+'"><div class="repeated" ng-repeat="n in [] | range:10">...</div></div>';varelement=angular.element(html);angular.element(document.body).append(html);test.directive(element,test.scope(),function(element){varelm=element[0];setTimeout(function(){varkids=elm.getElementsByTagName('div');expect(kids.length).to.equal(10);done();},1000);});});});
// 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);});});
TDD 또는 BDD로 진행을 하는 것은 소프트웨어의 명세를 만들어 가면서 보다 안정적인 서비스를 만드는 지름길이다. 물론 좀 더 시간을 투자해야하는 지름길이지만 나중의 유지보수를 생각하면 긴안목에서 역시 빠른 지름길이다. 특히 모바일 서비스의 경우는 몇번의 에러가 발생하면 사용자는 해당 앱을 삭제하고 절대로 사용하지 않는다는 보고가 있다. 그리고 페이스북에는 QA조직이 없고 개발자가 직접 모든 테스트를 수행하고 책임을 지도록 되어있다.
우리가 생산성을 위하여 프레임워크를 사용하듯, 안정성을 위해서는 테스트 코드가 필수이다. 또한 테스트 코드에 주석을 달고 Groc 같은 유틸로 문서화를 한다면 소프트웨어의 명세를 따로 하지 않아도 될 것이다.
백본기반의 개발에 있어서도 모델, 뷰, 라우터등의 TDD는 필요하며 이를 어떻게 하는지 따라해 보자
- profile.js 의 내역을 구현지 않았기 때문에 테스트에 대한 결과는 빨간색 fail
- profile.js 의 render() 메소드를 구현한다
(function(global, $, _, Backbone, undefined) {
app.views.Profile = Backbone.View.extend({
render: function() {
var html = "<h1>"+ this.model.getFullName() +"</h1>" +
"<p>"+ this.model.get('first_name') +" is "+ this.model.get('age') + " years old";
this.$el.html(html);
}
});
})(this, $, _, Backbone);
- 브라우져 호출 결과
테스트 코드를 만들고 실행하여 에러(빨간색)가 발생하면 테스트 코드에 대응하는 업무 코드를 짜고, 다시 실행하여 정상(녹색)적으로 돌아가게 한다. 정상에는 두 단계가 있는데, 비즈니스 로직이 없이 API와 정적값을 통해 정상(녹색) 통과를 하고 나면 이후 비즈니스 로직을 넣고 다시 정상(녹색) 통과를 하는지 체크한다.
JavaScript 의 Unit Test를 위하여 여러 프레임워크가 나왔고, 이중 자유도가 높은 Mocha를 사용하려 한다. Assert에 대해 변경을 하면 BDD, TDD를 할 수 있고, JavaScript Funcational 프로그래밍 테스트에 잘 어울리는 BDD를 기본으로 사용한다
JMeter에 Thread Group을 "Google Search"로 설정하고, WorkBeanch에서 레코딩 작업을 "Goolge Search"에 해준후 부하를 어떻게 쏘고 결과를 보는지 알아보자
▶ 부하 주기
Assertion을 설치하여 결과값이 제대로 레코딩된 것인지 확인 할 수 있다 ([Add] 버튼 클릭후 입력값 beethoven 넣음)
결과를 Graph Result로 보기위하여 추가한다
Thread Group의 user 수를 100으로 한다
상단의 메뉴 Run/Start를 수행한다
Graph Results 를 통하여 결과 성능을 볼 수 있다. (X축 : 동시 접속자수, Y축 : 응답시간) 성능그래프는 어차피 x축 동접수 대비 y축의 최대 응답시간의 직교점을 찾으면 된다.
Summary Report를 통하여 에러와 결과값을 수치로 확인한다
가장 간단하게 성능 테스트 하는 것을 보았는데, 예전에 사용한 MS Stress Test Tool과 유사하다. 하지만 Thread Group의 Add 항목에 있는 기능들을 보면 스크립트를(BeanShell) 이용하여 동적인 아규먼트를 주거나 다양한 프로토콜 (FTP, TCP, LDAP 등등) 을 지원한다. 따라서 사용수준의 사용법을 익히 알고 있는 엔지니어라면 -예로 LoadRunner- JMeter를 사용하는데 큰 어려움을 없을 것으로 생각된다.
The Apache JMeter™ desktop application is open source software, a 100% pure Java application designed to load test functional behavior and measure performance. It was originally designed for testing Web Applications but has since expanded to other test functions.
2. 특정 디렉토리에 복사를하고 classpath 환경변수에 잡아준다. (안 잡을 경우 java -classpath에서 잡아줌)
3. listen port를 지정하고 fitnesse 서버를 기동시킨다
4. 브라우져에서 확인한다. FitNesse는 Wiki Web Server 이다. 여기에 Fit 테스트를 결합 시켰다.
5. Main 페이지에서 왼쪽 메뉴에서 [Edit] 를 클릭한다
6. Editing 페이지 하단에 >TestSample 이라고 입력하고 Save한다
7. 화면의 하단의 TestSample[?] 에서 링크된 ? 를 클릭한 . 8. TestSample Wiki 페이지가 나온다.
9. Excel을 이용하여 StressTest Column Fixture를 만든다.
(상단의 명칭은 java 클래스 명칭과 동일하게 작성한다)
10. TestSample Wiki 페이지 왼쪽 메뉴 [Edit] 클릭하여 들어간다. 11. 편집모드에서 상단에 fitnesse.jar 파일이 있는 위치정보를 넣어주고, Excel표는 Copy하여 Paste 한후에 [SpreadSheet to FitNesse] 버튼을 클릭한다. 그러면 Excel 포멧이 FitNesse 포멧으로 변경된다. 그리고 [Save] 한다.
12. 저장한 화면이 다음과 같이 나온다
13. 왼쪽 메뉴 [Properties]에서 Test 속성을 체크하고 [Save] 한다
14. StressTest.java 파일을 작성한다
15. StressTest.java파일은 fitnesses.jar 파일이 있는 곳에서 컴파일 한다
16. 이제 모든 준비가 끝났다.
- 샘플 wiki 페이지를 만듦
- 샘플 wiki 내용에 인수 테이블 포멧 Copy&Paste from Excel
- Java Fixture를 만들어서 Fitnesse.jar 파일위치에서 컴파일
17. 다음에 TestSample 페이지의 [Test] 버튼을 클릭한다
18. 결과값이 자동으로 출력된다
한번의 스텝이 돌았다. 이제 FrontPage 메인에 또 다른 테스트 케이스를 > 를 이용하여 넣고서 첨부 할 수 있다.
4. fit-java-1.1.zip파일 압축을 풀면 폴더안에 : fit.jar 파일이 있다.
5. fit.jar 파일을 환경변수에 classpath = .;<directory>\fit.jar 설정을 한다. (설정하지 않으면 java 수행시 -classpath를 늘 잡아준다)
6. word를 열어서 다음과 같이 표를 만들어 .htm으로 저장한다
7. java 파일을 하기와 같이 만든다. (주의:htm파일 내역중 fixture명칭 CalculateDiscount 명칭과 java파일 명칭과 일치해야함)
8. eclipse에 만든 *.java 파일을 TestDiscount.htm 파일이 있는 곳에 위치 시킨다. 예) d:/Test_framework/fit_testing/TestCase-1
9. d:/Test_framework/fit_testing/TestCase-1 > javac *.java 명령으로 java 파일을 컴파일 한다. 10. d:/Test_framework/fit_testing/TestCase-1 > java fit.FileRunner TestDiscount.htm result.htm 명령을 수행한다.
11. 결과 result.htm 파일을 열어본다.
12. 1000에서 결과값을 0.00 으로 기대했지만 50.0 값이 나왔고, 밑에도 50.51을 기대했지만 50.5가 나와서 amount라는 인수값을 discount()라는 메소드에 대입하였을 때 인수 테스트 2개에 오류 (2 wrong)가 있었음을 result.htm 결과 화면에 표현해 준다.