HTML5 toolkit
Testing

Testing UI

Unit test - Gulp + Karma + Jasmine + Istanbul

Acceptance test - CucumberJs + Selenium Web drive

Unit tests beggining

have way more examples of unit test

cover base code parts  instead of selenium

What we need to:

deal with higher complexity code parts

Unit test

Unit test example:

'use strict';

var LivePreviewModule = require('src/app/workspace/live-preview/module');

describe('LivePreview', function(){

    /*
     * Test cases
     */


    it('should LivePreviewModule to be defined', function() {
        // Expectations
        expect(LivePreviewModule).toBeDefined();
    });

});

Unit tests mocks, stabs

'use strict';

var ComponentsServiceModule = require('src/app/components/components-service/module.js');
var componentsMock = require('test/unit/components/mockComponents.js');

describe('ComponentsService', function() {
    var q,
        deferred,
        FileContentGetDeferred,
        FileContentSaveTextFileDeferred,
        RESTServiceMock,
        ComponentManagerMock,
        scope,
        componentsService,
        sourceFileMock,
        FileContentMock,
        ArrayUtilsMock,
        UiBlockingServiceMock,
        TemplatesDomMock,
        BannerServiceMock,
        componentSettingsMock,
        expectedComponents;

    beforeEach(function() {
        expectedComponents = {
            'id1': {},
            'id2': {}
        };

        componentSettingsMock = {};

        RESTServiceMock = {
            getBanner: function() {
                deferred = q.defer();
                return deferred.promise;
            },
            updateAsset: function() {}
        };

        BannerServiceMock = {
            getBanner: function() {
                return {
                    assets: [{
                        source: 'fileId',
                        clicktags: []
                    }]
                };
            }
        };

        ComponentManagerMock = {
            register: function() {},
            registry: {
                'animation':   { hidden: true },
                'positioning': { hidden: true },
                'clicktag':    { hidden: true }
            },
            getComponents: function() {
                return expectedComponents;
            },
            read: function() {
                return componentSettingsMock;
            },
            remove: function() {},
            write: function() {}
        };

        FileContentMock = {
            get: function() {
                FileContentGetDeferred = q.defer();
                return FileContentGetDeferred.promise;
            },
            saveTextFile: function() {
                FileContentSaveTextFileDeferred = q.defer();
                return FileContentSaveTextFileDeferred.promise;
            }
        };

        ArrayUtilsMock = {
            removeObjectByProperty: function(array, property, value) {
                for (var i = 0, l = array.length; i < l; i++) {
                    if (array[i][property] === value) {
                        return array.splice(i, 1);
                    }
                }
            },
            findObjectByProperty: function() { return undefined; },
            findObjectIndexByProperty: function() { return -1; }
        };

        UiBlockingServiceMock = {
            'enable': function() {},
            'disable': function() {}
        };

        TemplatesDomMock = {
            create: function() {
                return {
                    reset: function() {},
                    render: function() {}
                };
            }
        };

        sourceFileMock = {
            id: 'fileId'
        };

        angular.mock.module(ComponentsServiceModule);
        angular.mock.module(function ($provide) {
            $provide.value('ComponentsRegistry', componentsMock);
            $provide.value('ComponentManager', ComponentManagerMock);
            $provide.value('TemplatesDOM', TemplatesDomMock);
            $provide.value('FileContent', FileContentMock);
            $provide.value('ArrayUtils', ArrayUtilsMock);
            $provide.value('BannerService', BannerServiceMock);
            $provide.value('RESTService', RESTServiceMock);
            $provide.value('UiBlockingService', UiBlockingServiceMock);
        });

        spyOn(ComponentManagerMock, 'register').and.callThrough();
        spyOn(ComponentManagerMock, 'read').and.callThrough();
        spyOn(FileContentMock, 'get').and.callThrough();
        spyOn(FileContentMock, 'saveTextFile').and.callThrough();
        spyOn(UiBlockingServiceMock, 'enable').and.callThrough();
        spyOn(UiBlockingServiceMock, 'disable').and.callThrough();

        inject(function(ComponentsService, $q, $rootScope) {
            componentsService = ComponentsService;
            q = $q;
            scope = $rootScope.$new();
        });

    });

    it('should register all components', function() {
        expect(ComponentManagerMock.register.calls.count()).toEqual(componentsMock.length);
    });

    describe('loadComponents, getComponents', function() {

        var promise,
            resolvedComponents;

        beforeEach(function() {
            promise = componentsService.loadComponents(sourceFileMock);
            FileContentGetDeferred.resolve('');

            promise.then(function() {
                resolvedComponents = componentsService.getComponents();
            });
        });

        it('should load components from given file', function() {

            var expectedComponentsLength;

            expectedComponents = {
                'id1': {},
                'id2': {},
                'id3': {}
            };

            expectedComponentsLength = Object.keys(expectedComponents).length;

            scope.$apply();

            expect(resolvedComponents.length).toEqual(expectedComponentsLength);
        });

        it('first loaded component should have 2 children', function() {

            expectedComponents = {
                'id1': { id: 'id1'},
                'id2': { id: 'id2', pid: 'id1' },
                'id3': { id: 'id3', pid: 'id1' }
            };

            scope.$apply();

            expect(resolvedComponents[0].$children.length).toEqual(2);
        });
    });

    describe('saveComponents', function() {

        beforeEach(function() {
            componentsService.loadComponents(sourceFileMock);
            FileContentGetDeferred.resolve('');

            expectedComponents = {
                'id1': { id: 'id1', type: 'button'},
                'id2': { id: 'id2', pid: 'id1', type: 'positioning' },
                'id3': { id: 'id3', pid: 'id1', type: 'animation' },
                'id4': { id: 'id4', pid: 'id1', type: 'clicktag' }
            };

            scope.$apply();
        });

        it('should save components to file', function() {
            componentsService.saveComponents();

            expect(FileContentMock.saveTextFile.calls.count()).toEqual(1);
        });

        it('should block UI while saving components', function() {
            componentsService.saveComponents();

            expect(UiBlockingServiceMock.enable.calls.count()).toEqual(1);
        });

        it('should unblock UI after successful save', function() {
            componentsService.saveComponents();

            FileContentSaveTextFileDeferred.resolve();
            scope.$apply();

            expect(UiBlockingServiceMock.disable.calls.count()).toEqual(1);
        });

        it('should reset components model after successful save', function() {
            var model = componentsService.getModel();
            model.changes = 'notVisible';

            componentsService.saveComponents();

            FileContentSaveTextFileDeferred.resolve();
            scope.$apply();

            expect(model.changes).toBe('none');
        });

        it('should keep selected component after save', function() {
            var model = componentsService.getModel(),
                lastSelected;

            model.selected = { id: 'componentId' };
            lastSelected = model.selected;

            componentsService.saveComponents();

            FileContentSaveTextFileDeferred.resolve();
            scope.$apply();

            expect(model.selected).toEqual(lastSelected);
        });

        it('should remove disabled components before save', function() {});
        it('should sync asset\'s and components\' clicktags before save', function() {});
    });

});

Writing Unit tests

From 32 test in 1 month without code coverage

To 157 in 5 month with code coverage

'use strict';
var WorkspaceModule = require('src/app/workspace/module.js');

describe('WorkspaceService', function() {
    var workspaceService;

    beforeEach(function() {
        angular.mock.module(WorkspaceModule);

        inject(function(WorkspaceService) {
            workspaceService = WorkspaceService;
        });
    });

    it('should be defined', function() {
        expect(workspaceService).toBeDefined();
    });

    it('should return model', function() {
        expect(workspaceService.getModel().topBar).toBeDefined();
    });

    it('should set leftSidebar active tab to settings',function() {
        workspaceService.openSidebar('settings');

        expect(workspaceService.getModel().rightSidebar.collapsed).toEqual(false);
        expect(workspaceService.getModel().leftSidebar.activeTab).toEqual('settings');
    });

    it('should set rightSidebar collapsed to true when toggle called second time',function() {
        workspaceService.toggleSidebar('settings');
        workspaceService.toggleSidebar('settings');

        expect(workspaceService.getModel().rightSidebar.collapsed).toEqual(true);
        expect(workspaceService.getModel().leftSidebar.activeTab).toEqual('');
    });

    it('should close active rightSidebar and open file-tree if leftSidebar from settings set to file-tree',function() {
        workspaceService.openSidebar('settings');
        workspaceService.openSidebar('file-tree');

        expect(workspaceService.getModel().rightSidebar.collapsed).toEqual(false);
        expect(workspaceService.getModel().leftSidebar.activeTab).toEqual('file-tree');
    });

});

Unit tests code coverage Istambul

Unit tests code coverage Istambul

Unit test optimization results

Change and structure code

Solved bigger complexity problems

 Allow us:

More information about selenium setup with cucumber
see in Vilmantas slides

Acceptance tests

Selenium running time was

From 15-20 min and a lot of failing tests

Before any changes:

How to improve?

Use more selenium instances 

Use separate selenium server

Make you environment Flexible

How to use more instance

So we have .feature files and have a lot of scenarios.

One scenario per single feature file one

We have 27 feature files

After splitting we have 106 feature/scenario files

And make "card dealing" to instance folder

What Gulp task do?

2. Read all feature scenario

1. Read all feature folders

3. Separate scenario to single files.

4. Move scenarios to instance folders

5. Run tests

6. Show output on demand "TeamCity or Local"

7. Clean instance folders after test done

Separate selenium server

On selenium instance for one cpu core?

From 16 core and 16 instances in 3-4 min stability issue.

4 instances and stable tests in 6-7min

Works for us

Flexible environment

Dev1 , Dev3 ,Preprod , Prod

Have ability to change environments

Change number of instances remotely and locally 

Run single test folder

Max 16 instances

Change test output

Default, Teamcity, debug

Cucumber Gulp task allowed us to: 

Flexible environment

Example:

gulp cucumber -e=DEV1 -i=4 -r=true -p=workspace --debug 

-e environment

-i instances

-p folder location

--debug show more detail information

Flexible environment

Local testing

Flexible environment

Remote testing

Flexible environment

Remote testing

Selenium server

Selenium server

Questions?

Adform HTML5 toolkit testing

By Darius Laurinčikas

Adform HTML5 toolkit testing

  • 1,103