Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to generate unit tests for existing Angular2 app with Jasmine Karma

I joined a team developing an Angular2 app that needs all the unit tests to be done with the Jasmine framework. I was wondering if there is a tool capable of generating spec files for each class (sort of a boiler plate code) by placing test cases based on available methods and/or based on attributes such as *ng-If in the templates. Here is an example of component a.component.js

import { Component, Input, Output, Inject, OnChanges, EventEmitter, OnInit } from '@angular/core';
import {Http} from '@angular/http';


@Component({
    selector: 'a-component',
    template : `
    <div *ng-If="model">
       <a-child-component [model]="model">
       </a-child-component>
    </div>`
})

export class AComponent implements OnInit {
    @Input() anInput;
    ngOnInit() {        
        if(this.anInput){
            this.model = anInput;
        }
    }
    constructor(@Inject(Http) http){
        this.restAPI = http;    
    }

    methodOne(arg1,arg2){
        //do something
    }

    methodTwo(arg1,arg2){
        //do something
    }

    //...
}

And generates a spec file : a.componenet.spec.js

import { beforeEach,beforeEachProviders,describe,expect,it,injectAsync } from 'angular2/testing';
import { setBaseTestProviders } from 'angular2/testing';
import { TEST_BROWSER_PLATFORM_PROVIDERS,TEST_BROWSER_APPLICATION_PROVIDERS } from 'angular2/platform/testing/browser';
setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS, TEST_BROWSER_APPLICATION_PROVIDERS);
import { Component, Input, Output, Inject, OnChanges, EventEmitter, OnInit } from '@angular/core';
import { ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { MockComponent } from 'ng2-mock-component';
import { async } from '@angular/core/testing';
import { Http } from '@angular/http';
import { HttpMock } from '../mocks/http.mock';
import { AComponent } from './a.component';

let model = {"propOne":[],"propTwo":"valueTwo"};

describe('AComponent', () => {
  let fixture;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [
            AComponent,
            MockComponent({ 
                selector: 'a-child-component',
                template:'Hello Dad!'
                ,inputs: ['model']
            })
       ],
        providers: [{ provide: Http, useClass: HttpMock }]
    });
    fixture = TestBed.createComponent(AComponent);
    fixture.componentInstance.anInput= model;    
  });

  it('should create the component',() => {
    //
  });
  it('should test methodOne',() => {
    //
  });
  it('should test methodTwo',() => {
    //
  });
  it('should generate the child component when model is populated',() => {
    //
  });
)
like image 405
Mehdi Avatar asked Feb 21 '17 02:02

Mehdi


3 Answers

It has been a while since I posted this question. I have developed a visual Code extension to help with this task that I want to share with you. The point of this extension is not just to create the spec file, it also generates some boiler plate code for all the test cases you need to write. It also creates the Mocks and injections you need to get you going faster. it adds a test case that will fail if you haven't implemented all the tests. Fell free to remove it if it doesn't fit your needs. This was done for an Angular2 ES6 project but you can update it for typescript as you wish :

// description : This extension will create a spec file for a given js file. // if the the js file is an angular2 componenet, it will then look for the html template and create a spec file containing Mock componenet class for each child included in the html

var vscode = require('vscode');
var fs = require("fs");
var path = require("path");

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
function activate(context) {
    var disposable = vscode.commands.registerCommand('extension.unitTestMe', function () {
        // The code you place here will be executed every time your command is executed
        var htmlTags = ['h1','h2','h3','h4','h5','a','abbr','acronym','address','applet','area','article','aside','audio','b','base','basefont','bdi','bdo','bgsound','big','blink','blockquote','body','br','button','canvas','caption','center','cite','code','col','colgroup','command','content','data','datalist','dd','del','details','dfn','dialog','dir','div','dl','dt','element','em','embed','fieldset','figcaption','figure','font','footer','form','frame','frameset','head','header','hgroup','hr','html','i','iframe','image','img','input','ins','isindex','kbd','keygen','label','legend','li','link','listing','main','map','mark','marquee','menu','menuitem','meta','meter','multicol','nav','nobr','noembed','noframes','noscript','object','ol','optgroup','option','output','p','param','picture','plaintext','pre','progress','q','rp','rt','rtc','ruby','s','samp','script','section','select','shadow','slot','small','source','spacer','span','strike','strong','style','sub','summary','sup','table','tbody','td','template','textarea','tfoot','th','thead','time','title','tr','track','tt','u','ul','var','video','wbr'];
        var filePath;
        var fileName;
        if(vscode.window.activeTextEditor){
            filePath = vscode.window.activeTextEditor.document.fileName;
            fileName = path.basename(filePath);
            if(fileName.lastIndexOf('.spec.') > -1 || fileName.lastIndexOf('.js') === -1 || fileName.substring(fileName.lastIndexOf('.js'),fileName.length) !== '.js'){
                vscode.window.showErrorMessage('Please call this extension on a Javascript file');
            }else{
               var splitedName = fileName.split('.');
                splitedName.pop();
                var capitalizedNames = [];
                splitedName.forEach(e => {
                    capitalizedNames.push(e.replace(e[0],e[0].toUpperCase()));
                });
                var className = capitalizedNames.join('');

                // ask for filename
                // var inputOptions =  {
                //     prompt: "Please enter the name of the class you want to create a unit test for",
                //     value: className
                // };
                // vscode.window.showInputBox(inputOptions).then(className => {
                let pathToTemplate; 
                let worspacePath = vscode.workspace.rootPath;
                let fileContents = fs.readFileSync(filePath);
                let importFilePath = filePath.substring(filePath.lastIndexOf('\\')+1,filePath.lastIndexOf('.js'));
                let fileContentString = fileContents.toString();
                let currentFileLevel = (filePath.substring(worspacePath.length,filePath.lenght).match(new RegExp("\\\\", "g")) || []).length;
                let htmlFile;
                if(fileContentString.indexOf('@Component({') > 0){
                    pathToTemplate = worspacePath + "\\unit-test-templates\\component.txt";
                    htmlFile = filePath.replace('.js','.html');
                }else if(fileContentString.indexOf('@Injectable()') > 0){
                    pathToTemplate = worspacePath + "\\unit-test-templates\\injectableObject.txt";
                }
                let fileTemplatebits = fs.readFileSync(pathToTemplate);
                let fileTemplate = fileTemplatebits.toString();
                let level0,level1;
                switch(currentFileLevel){
                    case 1:
                        level0 = '.';
                        level1 = './client';
                    break;
                    case 2:
                        level0 = '..';
                        level1 = '.';
                    break;
                    case 3:
                        level0 = '../..';
                        level1 = '..';
                    break;
                }

                fileTemplate = fileTemplate.replace(/(ComponentName)/g,className).replace(/(pathtocomponent)/g,importFilePath);
                //fileTemplate = fileTemplate.replace(/(pathtocomponent)/g,importFilePath);
                //let templateFile = path.join(templatesManager.getTemplatesDir(), path.basename(filePath));
                let templateFile = filePath.replace('.js','.spec.js');
                if(htmlFile){
                    let htmlTemplatebits = fs.readFileSync(htmlFile);
                    let htmlTemplate = htmlTemplatebits.toString();
                    let componentsUsed = htmlTemplate.match(/(<[a-z0-9]+)(-[a-z]+){0,4}/g) || [];//This will retrieve the list of html tags in the html template of the component.
                    let inputs = htmlTemplate.match(/\[([a-zA-Z0-9]+)\]/g) || [];//This will retrieve the list of Input() variables of child Components
                    for(var q=0;q<inputs.length;q++){
                        inputs[q] = inputs[q].substring(1,inputs[q].length -1);
                    }
                    if(componentsUsed && componentsUsed.length){
                        for(var k=0;k<componentsUsed.length;k++){
                            componentsUsed[k] = componentsUsed[k].replace('<','');
                        }
                        componentsUsed = componentsUsed.filter(e => htmlTags.indexOf(e) == -1);
                        if(componentsUsed.length){
                            componentsUsed = componentsUsed.filter((item, pos,self) =>{
                                return self.indexOf(item) == pos;//remove duplicate 
                            });
                            let MockNames = [];
                            componentsUsed.forEach(e => {
                                var splitedTagNames = e.split('-');
                                if(splitedTagNames && splitedTagNames.length > 1){
                                    var capitalizedTagNames = [];
                                    splitedTagNames.forEach(f => {
                                        capitalizedTagNames.push(f.replace(f[0],f[0].toUpperCase()));
                                    });
                                    MockNames.push('Mock' + capitalizedTagNames.join(''));
                                }else{
                                    MockNames.push('Mock' + e.replace(e[0],e[0].toUpperCase()));
                                }
                            })
                            let MockDeclarationTemplatebits = fs.readFileSync(worspacePath + "\\unit-test-templates\\mockInportTemplace.txt");
                            let MockDeclarationTemplate = MockDeclarationTemplatebits.toString();
                            let inputList = '';
                            if(inputs && inputs.length){
                                inputs = inputs.filter(put => put !== 'hidden');                    
                                inputs = inputs.filter((item, pos,self) =>{
                                    return self.indexOf(item) == pos;//remove duplicate 
                                });
                                inputs.forEach(put =>{
                                    inputList += '@Input() ' + put + ';\r\n\t'    
                                });
                            }
                            let declarations = '';
                            for(var i=0;i < componentsUsed.length; i++){
                                if(i != 0){
                                    declarations += '\r\n';
                                }
                                declarations += MockDeclarationTemplate.replace('SELECTORPLACEHOLDER',componentsUsed[i]).replace('MOCKNAMEPLACEHOLDER',MockNames[i]).replace('HTMLTEMPLATEPLACEHOLDER',MockNames[i]).replace('ALLINPUTSPLACEHOLDER',inputList);
                            }
                            fileTemplate = fileTemplate.replace('MockComponentsPlaceHolder',declarations);
                            fileTemplate = fileTemplate.replace('ComponentsToImportPlaceHolder',MockNames.join(','));
                        }else{
                            fileTemplate = fileTemplate.replace('MockComponentsPlaceHolder','');
                            fileTemplate = fileTemplate.replace(',ComponentsToImportPlaceHolder','');
                        }

                    }else{
                        fileTemplate = fileTemplate.replace('MockComponentsPlaceHolder','');
                        fileTemplate = fileTemplate.replace(',ComponentsToImportPlaceHolder','');
                    }        
                }else{
                    fileTemplate = fileTemplate.replace('MockComponentsPlaceHolder','');
                    fileTemplate = fileTemplate.replace(',ComponentsToImportPlaceHolder','');        
                }
                fileTemplate = fileTemplate.replace(/(LEVEL0)/g,level0).replace(/(LEVEL1)/g,level1);
                if(fs.existsSync(templateFile)){
                    vscode.window.showErrorMessage('A spec file with the same name already exists. Please rename it or delete first.');
                }else{
                    fs.writeFile(templateFile, fileTemplate, function (err) {
                        if (err) {
                                vscode.window.showErrorMessage(err.message);
                            } else {
                                vscode.window.showInformationMessage("The spec file has been created next to the current file");
                            }
                    });
                } 
            }
        }else{
            vscode.window.showErrorMessage('Please call this extension on a Javascript file');
        }
    });
    context.subscriptions.push(disposable);
}
exports.activate = activate;

// this method is called when your extension is deactivated
function deactivate() {
}
exports.deactivate = deactivate;

For this to work, you need 2 template files, one for components, and one for injectable services. You can add pipes and other type of TS classes

component.txt template :

/**
 * Created by mxtano on 10/02/2017.
 */
import { beforeEach,beforeEachProviders,describe,expect,it,injectAsync } from 'angular2/testing';
import { setBaseTestProviders } from 'angular2/testing';
import { TEST_BROWSER_PLATFORM_PROVIDERS,TEST_BROWSER_APPLICATION_PROVIDERS } from 'angular2/platform/testing/browser';
setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS, TEST_BROWSER_APPLICATION_PROVIDERS);
import { Component, Input, Output, Inject, OnChanges, EventEmitter, OnInit } from '@angular/core';
import { ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { async } from '@angular/core/testing';
import { YourService} from 'LEVEL1/service/your.service';
import { YourServiceMock } from 'LEVEL0/test-mock-class/your.service.mock';
import { ApiMockDataIfNeeded } from 'LEVEL0/test-mock-class/apiMockData';
import { FormBuilderMock } from 'LEVEL0/test-mock-class/form.builder.mock';
import { MockNoteEventController } from 'LEVEL0/test-mock-class/note.event.controller.mock';    
import { ComponentName } from './pathtocomponent';


MockComponentsPlaceHolder

describe('ComponentName', () => {
  let fixture;
  let ListOfFunctionsTested = [];
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [
            ComponentName
            ,ComponentsToImportPlaceHolder
        ],
        providers: [
            //Use the appropriate class to be injected
            //{provide: YourService, useClass: YourServiceMock}                
            ]
    });
    fixture = TestBed.createComponent(ComponentName);    
    //Insert initialising variables here if any (such as as link or model...)
  });

  //This following test will generate in the console a unit test for each function of this class except for constructor() and ngOnInit()
  //Run this test only to generate the cases to be tested.
  it('should list all methods', async( () => {
        //console.log(fixture.componentInstance);
        let array = Object.getOwnPropertyNames(fixture.componentInstance.__proto__);
        let STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
        let ARGUMENT_NAMES = /([^\s,]+)/g;        
        array.forEach(item => {
                if(typeof(fixture.componentInstance.__proto__[item]) === 'function' && item !== 'constructor' && item !== 'ngOnInit'){
                    var fnStr = fixture.componentInstance.__proto__[item].toString().replace(STRIP_COMMENTS, '');
                    var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
                    if(result === null)
                        result = [];
                    var fn_arguments = "'"+result.toString().replace(/,/g,"','")+"'";
                    console.log("it('Should test "+item+"',()=>{\r\n\tListOfFunctionsTested.push('"+item+"');\r\n\t//expect(fixture.componentInstance."+item+"("+fn_arguments+")).toBe('SomeValue');\r\n});");
                }
        });
        expect(1).toBe(1);
    }));


    //This test will make sure that all methods of this class have at leaset one test case 
    it('Should make sure we tested all methods of this class',() =>{
        let fn_array = Object.getOwnPropertyNames(fixture.componentInstance.__proto__);
        fn_array.forEach(fn=>{
            if(typeof(fixture.componentInstance.__proto__[fn]) === 'function' && fn !== 'constructor' && fn !== 'ngOnInit'){
                if(ListOfFunctionsTested.indexOf(fn)=== -1){
                    //this test will fail but will display which method is missing on the test cases.
                    expect(fn).toBe('part of the tests. Please add ',fn,' to your tests');
                }
            }
        });
    })

});

Here is a template for Mock Components referenced by the extension mockInportTemplace.txt:

@Component({
  selector: 'SELECTORPLACEHOLDER',
  template: 'HTMLTEMPLATEPLACEHOLDER'
})
export class MOCKNAMEPLACEHOLDER {
  //Add @Input() variables here if necessary
  ALLINPUTSPLACEHOLDER
}

Here is template referenced by the extension for injectables:

import { beforeEach,beforeEachProviders,describe,expect,it,injectAsync } from 'angular2/testing';
import { setBaseTestProviders } from 'angular2/testing';
import { TEST_BROWSER_PLATFORM_PROVIDERS,TEST_BROWSER_APPLICATION_PROVIDERS } from 'angular2/platform/testing/browser';
setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS, TEST_BROWSER_APPLICATION_PROVIDERS);
import { Component, Input, Output, Inject, OnChanges, EventEmitter, OnInit } from '@angular/core';
import { ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { async } from '@angular/core/testing';
import { RestAPIMock } from 'LEVEL0/test-mock-class/rest.factory.mock';
import {Http} from '@angular/http';
//import { Subject } from 'rxjs/Subject';
import { ComponentName } from './pathtocomponent';
import { ApiMockData } from 'LEVEL0/test-mock-class/ApiMockData';

describe('ComponentName', () => {
    let objInstance;
    let service;
    let backend;
    let ListOfFunctionsTested = [];
    let singleResponse  = { "properties": {"id": 16, "partyTypeId": 2, "doNotContact": false, "doNotContactReasonId": null, "salutationId": 1}};
    let restResponse = [singleResponse];    

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                ComponentName
                //Here you declare and replace an injected class by its mock object
                //,{ provide: Http, useClass: RestAPIMock }
            ]
        });
    });


    beforeEach(inject([ComponentName
                      //Here you can add the name of the class that your object receives as Injection 
                      //  , InjectedClass
                      ], (objInstanceParam
                         //   , injectedObject
                         ) => {
        objInstance = objInstanceParam;
        //objInstance.injectedStuff = injectedObject;
    }));

    it('should generate test cases for all methods available', () => {
        let array = Object.getOwnPropertyNames(objInstance.__proto__);
        let STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
        let ARGUMENT_NAMES = /([^\s,]+)/g;        
        array.forEach(item => {
                if(typeof(objInstance.__proto__[item]) === 'function' && item !== 'constructor' && item !== 'ngOnInit'){
                    var fnStr = objInstance.__proto__[item].toString().replace(STRIP_COMMENTS, '');
                    var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
                    if(result === null)
                        result = [];
                    var fn_arguments = "'"+result.toString().replace(/,/g,"','")+"'";
                    console.log("it('Should test "+item+"',()=>{\r\n\tListOfFunctionsTested.push('"+item+"');\r\n\t//expect(objInstance."+item+"("+fn_arguments+")).toBe('SomeValue');\r\n});");
                }
        });
        expect(1).toBe(1);
    });

    //This test will make sure that all methods of this class have at leaset one test case 
    it('Should make sure we tested all methods of this class',() =>{
        let fn_array = Object.getOwnPropertyNames(objInstance.__proto__);
        fn_array.forEach(fn=>{
            if(typeof(objInstance.__proto__[fn]) === 'function' && fn !== 'constructor' && fn !== 'ngOnInit'){
                if(ListOfFunctionsTested.indexOf(fn)=== -1){
                    //this test will fail but will display which method is missing on the test cases.
                    expect(fn).toBe('part of the tests. Please add ',fn,' to your tests');
                }
            }
        });
    })


});

The three files above need to live inside your project under src in a folder referenced as unit-test-templates

Once you create this extension in you visual code, go to a JS file you want to generate unit test for, press F1, and type UniteTestMe. make sure there isn't a spec file created already.

like image 179
Mehdi Avatar answered Nov 02 '22 04:11

Mehdi


There is a paid visual studio extension called Simon test, please find the link below https://marketplace.visualstudio.com/items?itemName=SimonTest.simontest It gives the option to generate unit test with boiler plate code. The trail period for this extension is 30 days.

like image 29
Kallam Avatar answered Nov 02 '22 06:11

Kallam


I've tested ngx-spec using Jasmine/Karma, and it works:

Angular CLI create .spec files for already existing components


I also tested ngentest with Jasmine/Karma and it didn't work for me

Stackoverflow question for the issue:

error TS1127: Invalid character when running Karma tests in Angular 7

Github issue

https://github.com/allenhwkim/ngentest/issues/17

like image 43
Chris Halcrow Avatar answered Nov 02 '22 04:11

Chris Halcrow