เริ่มต้นเขียน unit test กับ JavaScript AngularJS ตอนที่ 5 unit test directive ทีมี external template

จากบทความที่ผ่านมา เราได้เรียนรู้การเขียน unit test directive เบื้องต้นไปแล้ว directive ที่เราสร้างมักจะมี template อยู่ด้วย เป็น HTML ต่างๆ เพื่อแสดงเป็น UI ของ directive

ถ้า template ของเราเป็น html ไม่เยอะมาก การจับ template เข้าไปรวมอยู่กับ directive file ก็คงไม่มีปัญหา อย่างเช่นที่เราทำกันในตัวอย่างทีาผ่านมา

แต่ชีวิตจริง เราอาจจะมี directive ที่ยาวกว่านี้ เช่นใน codesanook ผมก็สร้างพวกปุ่ม editor เป็น markdown html template จึงค่อนข้างยาวจะให้ใส่เข้าไปกับ directive คงไม่สะดวก แยกออกมาเป็น file จะดีกว่า

ดังนั้นให้เรามันจะสร้าง directive ด้วยการกำหนดค่า templateUrl เพื่อแยก template ออกมาอีก file

โดยเราเราจะเรียนเรียนรู้กันดังนี้

  • สร้าง directive ใหม่จาก directive ที่มีอยู่ แต่ไปใช้ templateUrl แทน
  • สร้าง external template ให้กับ directive
  • สร้าง test script ทดสองการทำงานของ directive
  • ทดสอบ run unit test
  • ติดตั้ง karma plugin
  • ทดสอบ run unit test อีกครั้ง

สร้าง directive ใหม่จาก directive ที่มีอยู่ แต่ไปใช้ templateUrl แทน

สร้าง file emailSubscriptionDirectiveExternalTemplate.js โดย copy จาก emailSubscriptionDirective.js วางไว้ใน folder src แล้วแก้ไขคำสั่งเป็นดังนี้

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

app.directive('emailSubscriptionExternalTemplate', function () {

    var emailSuscriptionDirectiveExternalTemplate = {
        restrict: 'E',
        scope: {
            email: "="
        },

        controller: function ($scope) {
            $scope.hasSubscribed = false;

            $scope.subscribe = function () {
                $scope.hasSubscribed = true;
                console.log("Thank you for subscribing, we will send email to " + $scope.email);
            };
        },

        templateUrl: 'emailSubscriptionTemplate.html'
    };

    return  emailSuscriptionDirectiveExternalTemplate;
});

อธิบาย

  • เราได้เปลียนชื่อ module ใหม่เป็น emailSubscriptionExternalTemplateTestApp และ directive ใหม่เป็น emailSubscriptionExternalTemplate
  • เปลี่ยนการกำหนดค่า template มาเป็น templateUrl แทน และระบุ path ที่เราเก็บ template file ที่เรากำลังจะสร้างกัน

src/emailSubscriptionTemplate.html

<div>
    <input id="txtEmail" type="text" ng-model="email"/>
</div>
<div>
    <input id="btnSubscribe" type="button" ng-click="subscribe()"/>
</div>

สร้าง test script ทดสองการทำงานของ directive

สร้าง test script file ใหม่โดย copy จาก emailSubscriptionDirectiveSpec.js และตั้งชื่อว่า emailSubscriptionDirectiveExternalTemplateSpec.js เก็บไว้ใน folder spec แล้วแก้ไข file เป็นดังนี้

spec/emailSubscriptionDirectiveExternalTemplateSpec.js

describe("emailSubscriptionDirectiveExternalTemplate", function () {

    it('input elements should be defined', function () {
        var el = null;
        var scope = null;

        angular.mock.module('emailSubscriptionExternalTemplateTestApp');
        angular.mock.inject(function ($compile, $rootScope) {

            $rootScope.email = null;
            var htmlElement = angular.element(
                '<email-subscription-external-template email="email"></email-subscription>');

            el = $compile(htmlElement)($rootScope);
            $rootScope.$digest();

            scope = el.isolateScope();
        });

        var emailTextInput = el.find("#txtEmail");
        expect(emailTextInput).toBeDefined();
        expect(emailTextInput.attr('id')).toBe("txtEmail");

        var button = el.find("#btnSubscribe");
        expect(button).toBeDefined();
    });
});

อธิบายคำสั่ง

  • เราได้ load module emailSubscriptionExternalTemplateTestApp ที่มี emailSubscriptionExternalTemplate
  • inject $scompile และ $rootScope เพื่อสร้าง directive
  • ทดสอบว่า template โหลดเรียบร้อยไหม โดยการทดสอบ input element , textEmail, btnSubscribe สามารถดึงออกมาใช้งานได้ และมีค่าอยู่จริง

run unit test

เราจะมาทดสอบการทำงานของ directive กัน ว่าสามารถ load element ต่างๆ ที่อยู่ใน external template ได้ไหม โดยเข้าไปที่ root folder ของ project เปิด Windows command line หรือ Linux terminal และพิมพ์คำสั่งต่อไปนี้

npm test

ผลลัพธ์ที่ได้

image title

error แสดงออกมาเป็น Error: Unexpected request: GET emailSubscriptionTemplate.html หมายความว่า directive พยายามที่จะโหลด emailSubscriptionTemplate.html หากถ้าคำสั่งนี้ อยู่ใน production code ไม่ได้อยู่ใน unit test แบบนี้

การทดสอบโดยเปิด browser ปกติ Angular ก็จะโหลด template ได้ถูกต้อง แต่พออยู่ใน unit test เราไม่ทดสอบกับ external service เช่น http request, database access กันครับ เพราะทำให้ unit test ช้า และยากที่จะ clean up data ให้อยู่ใน state ที่เราต้องการ เราต้อง isolate พวกนี้ ออกไปโดยทำเป็น mock object แทน

ปัญหาของเราตอนนี้คือ directive โหลด external template ไม่ได้ ดังนั้น เราจะทำอย่างไรดีน้ออ

ก่อนอื่นเลย ให้เรามาทำความเข้าใจลักษณ์การทำงานของ exteranal template ก่อนครับ

  • Angular framework จะโหลด external template ผ่าน http request จากนั้นจะ compile template เป็น JavaScript object แล้วเก็บไว้เป็น cache เมื่อ directive จะใช้งาน template ก็ไปดึงจาก cache ด้วย key ที่เป็นค่าของ TemplateUrl
  • เราไม่ต้องทำขั้นตอนต่างๆ เหล่านี้เองเลย เพียงใช้ plugin ของ Karma ก็สามารถใช้จับ external template เข้า template cache ได้เลย

ติดตั้ง karma plugin

ให้เราติดตั้ง karma plugin ที่ชื่อว่า karma-ng-html2js-preprocessor

npm install karma-ng-html2js-preprocessor --save-dev

จากนั้นให้เข้าไปแก้ไข karma.config.js เป็นดังนี้ครับ

module.exports = function (config) {
    config.set({

        basePath: '',
        frameworks: ['jasmine'],

        files: [
            'lib/jquery-2.2.0.min.js',
            'lib/angular-1.5.0/angular.min.js',
            'lib/angular-1.5.0/angular-mocks.js',
            'src/**/*.js',
            'spec/**/*.js',

            'src/**/*.html'
        ],

        exclude: [],

        preprocessors: {
            'src/**/*.html': ['ng-html2js']
        },

        ngHtml2JsPreprocessor: {
            cacheIdFromPath: function (filePath) {

                console.log("filePath " + filePath);
                var cacheId = filePath.replace("src/", "");

                console.log("cacheId " + cacheId);
                return cacheId;
            },
            moduleName: 'ngTemplates'
        },

        reporters: ['progress'],

        port: 9876,

        colors: true,

        logLevel: config.LOG_INFO,

        autoWatch: true,

        browsers: ['PhantomJS'],

        singleRun: false,

        concurrency: Infinity
    })
};

  • สิ่งที่เราได้แก้ไขคือการ config ค่าให้ karma-ng-html2js-preprocessor ทำงานได้ถูกต้อง ไปโหลด html template มา compile เป็น JavaScript object แล้วเก็บไว้ใน template cached โดยค่าที่เรากำหนดมีด้วยกัน 3 ส่วน
  • files config value เพิ่ม 'src/**/*.html' เข้าไปเพื่อโหลด html เข้าไปใน test preprocessors กำหนดให้ file html จะต้องถูก preprocess ด้วย ng-html2js
  • ngHtml2JsPreprocessor config ค่าต่างๆ ในการ convert ตรงนี้มีส่วนที่น่าสนใจคือ cacheId ที่เราจะได้เข้าไปแก้ไข เนื่องจากเวลาที่ karma-ng-html2js-preprocessor compile html มาแล้ว จะสร้าง cacheId ตาม path ของ file ดังนั้นหากเราไม่มี cofig ส่วนนี้ cacheId ที่ได้ก็คือ src/emailSubscriptionTemplate.html แต่ใน directive กลับมองหา cacheId ที่มีค่า emailSubscriptionTemplate.html เราจะต้องทำให้ค่า cacheId ตรงกัน เพื่อไม่ให้ directive ไปโหลด http request เพราะไม่เจอใน cache พร้อมกำหนดค่า module ngTemplates เพื่ออ้างอิงถึง template ในตอนใช้งาน angular.mock.module()

เข้าไปแก้ไข file emailSubscriptionDirectiveExternalTemplate โดยเพิ่มโหลด ngTemplates module เข้าไป

angular.mock.module('emailSubscriptionExternalTemplateTestApp','ngTemplates');

ทดสอบ run unit test อีกครั้ง

หลังจากกำหนดค่าต่างๆ ของ karma.config.js แล้ว เรามาลองทดสอบการทำงานอีกครั้ง

npm test

image title

ผลลัพธ์การทำงานถูกต้อง

สังเกตว่าเราได้ log cacheId มาด้วย

ใครมีความเห็น คำแนะนำใดๆ เขียนเข้ามาได้เลยครับ ขอบคุณครับ

codesanook และไม่เครียดกับการ code นะครับ

download source code

เมื่อโหลดไปแล้วใช้คำสั่ง npm install package ต่างๆ ก็จะโหลดให้โดยอัตโนมัติครับ