Skip to content

Instantly share code, notes, and snippets.

@saconnolly
Last active May 28, 2020 14:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save saconnolly/619dfd855107b2e6c9bf9ab9257dd162 to your computer and use it in GitHub Desktop.
Save saconnolly/619dfd855107b2e6c9bf9ab9257dd162 to your computer and use it in GitHub Desktop.
NgUpgrade unit tests for components

Unit tests for services

Upgrading the services themselves has been a very straightforward experience using ngUpgrade. The issue that arose was when we had angularJs services that were injecting services that we had converted to Angular. Pete Bacon Darwin’s PR got me going in the right direction and after a few discussions with him (he was extremely helpful), this is where I landed (since I originally did this work, Pete's MR has been merged and can be seen here: https://angular.io/api/upgrade/static/testing/createAngularJSTestingModule):

const { angular } = window as any;  
const $INJECTOR = '$injector';  
const INJECTOR_KEY = '$$angularInjector';
export class TestHelpers {  
private static createAngularJSTestingModule(angularModules: any[]) {  
    return angular.module('$$angularJSTestingModule', []).factory(INJECTOR_KEY, [  
        $INJECTOR,  
        ($injector: any) => {  
            TestBed.configureTestingModule({  
                imports: angularModules,  
                providers: [  
                    {  
                        provide: $INJECTOR,
                        useFactory: () => $injector  
                    }  
                ]  
            });  
            return TestBed.get(Injector);  
            }  
    ]).name;  
  }  
  public static initAngularJSTestingModule() {  
    const testBed = getTestBed();  
    if (testBed.platform === null) {  
        testBed.initTestEnvironment(  
            BrowserDynamicTestingModule,
            platformBrowserDynamicTesting()  
        );  
    }  
   mock.module(TestHelpers.createAngularJSTestingModule([AngularModuleHere]));  
  }  
}

and then added global beforeEach for every test like

beforeEach(() => {
    TestHelpers.initAngularJSTestingModule();  
});

Unit tests for components

This is where things seem to get really tricky and I am currently stuck. I started with a simple angularJs directive that wraps our icons. The conversion was again extremely simple and the issue I am facing with the tests does not arise when running the app. Here are the scenarios:

  • An existing angularJs directive is using our pr-icon (Angular) component in its template but there are no specific tests around it.

    For these the fix was simple. In the same beforeEach that I am using for initializing the injector I just mock an empty pr-icon:

beforeEach(() => {      
    TestHelpers.initAngularJSTestingModule();  
    ng.mock.module(**AngularJsModuleName**, function($provide) {  
	    $provide.factory('prIconDirective', () => {  
            return {};  
        });  
    });  
});
  • An existing angularJs directive is using our pr-icon directive in its template and there are tests for it but nothing is dynamic.

    For this scenario I looked to the tests in the upgradeModule and adapted things just slightly. Here is a group of helper functions that can be used to get your tests going

export function bootstrap(
    platform: PlatformRef,
    Ng2Module: Type<{}>,
    element: any,
    modsArray: any
) {
    // We bootstrap the Angular module first; then when it is ready (async) we bootstrap the AngularJS
    // module on the bootstrap element (also ensuring that AngularJS errors will fail the test).
    return platform.bootstrapModule(Ng2Module).then(ref => {
        const ngZone = ref.injector.get<NgZone>(NgZone);
        const upgrade = ref.injector.get(UpgradeModule);
        ngZone.run(() => upgrade.bootstrap(element, modsArray));
        return upgrade;
    });
}

export function testSetup(modsToAdd?) {
    const failHardModule: any = ($provide: any) => {
        $provide.value('$exceptionHandler', (err: any) => {
            throw err;
        });
    };
    let modsArray = [failHardModule, **AngularJsModuleName**];
    modsToAdd && modsToAdd.forEach(mod => modsArray.push(mod.name));
    return bootstrap(platformBrowserDynamic(), **AngularModuleName**, '<div></div>', modsArray);
}

export function clearPlatforms() {
    beforeEach(() => destroyPlatform());
    afterEach(() => destroyPlatform());
}

export function compileAngularComponent(el, upgrade, scopeToUse?) {
    let rootScope = upgrade.$injector.get('$rootScope');
    let isolateScope = scopeToUse || rootScope.$new();
    let compile = upgrade.$injector.get('$compile');
    let compiledEl = compile(el)(isolateScope);
    isolateScope.$digest();
    return compiledEl;
}

export function getUpgradeObjects(upgrade, scopeToUse?) {
    let rootScope = upgrade.$injector.get('$rootScope');
    let isolateScope = scopeToUse || rootScope.$new();
    let compile = upgrade.$injector.get('$compile');
    return {
        compile: compile,
        scope: isolateScope
    };
}

and then we can do something like

describe('ICONS: ', function() {  
    let icon;  
    clearPlatforms();  
    it('verify our icon is in the DOM', async(() => {  
        let elemPrBtn = ng.element('<pr-btn name="primary-add"></pr-btn>'); 
        testSetup().then(upgrade => {  
            let elemCompiled = compileAngularComponent(elemPrBtn, upgrade);  
	        icon = elemCompiled.find('.pr-icon');  
	        expect(icon.length).toBe(1);  
        });  
  }));      
}); 

Above is a simple scenario where there aren't any dependencies being injected into the AngularJS directive/component that is being tested. In a more complex situation there are a few more steps. First we create a getter function that will get your dependencies using the $injector passed into it (this is an example, you only need to get the deps you need):

useCustomInjector = {
                get: function($injector) {
                    $compile = $injector.get('$compile');
                    $q = $injector.get('$q');
                    $log = $injector.get('$log');
                    $uibModal = $injector.get('$uibModal');
                    $state = $injector.get('$state');
                    $rootScope = $injector.get('$rootScope');
  		    myService = $injector.get('myService');	
                    return;
                }
            }

Then I create a getter for my spies:

getSpies = {
                get: function() {
                    spyOn(myService, 'search').and.returnValue($q.when(mockSearchResult));
                    spyOn($log, 'debug').and.returnValue('');
                    spyOn($log, 'info').and.returnValue('');
                    return;
                }
            }

Then in any of the describe blocks where this will be needed I start with:

clearPlatforms();

beforeEach(done => {
	testSetup([**AngularJSModuleName**]).then(upgrade => {
	    upgradeInjector = upgrade.$injector;
	    testingObj = getUpgradeObjects(upgrade);
	    useCustomInjector.get(upgradeInjector);
	    getSpies.get();
	    done();
	});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment