2017-02-21 55 views
5

我加入了开发Angular2应用程序的团队,该团队需要使用Jasmine框架完成所有单元测试。 我想知道是否有一种工具能够通过基于可用方法放置测试用例和/或基于属性(如模板中的* ng-If)为每个类生成spec文件(排序锅炉板代码)。 这里是组分的a.component.js如何使用Jasmine Karma为现有Angular2应用程序生成单元测试

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 
    } 

    //... 
} 

一个例子,并产生spec文件: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',() => { 
    // 
    }); 
) 

回答

1

它已经有一段时间,因为我发布了这个问题。我开发了一个视觉代码扩展来帮助完成我想与您分享的任务。 这个扩展的目的不仅仅是创建spec文件,它还为您需要编写的所有测试用例生成一些boiler plate代码。 它还创建了您需要的模拟和注射,以加快您的速度。 它增加了一个测试用例,如果你没有实现所有的测试,它将会失败。如果它不符合您的需求,请随时删除它。 这是为一个Angular2 ES6项目完成的,但您可以根据您的意愿更新它的打字稿:

//描述:该扩展将为给定的js文件创建一个spec文件。 //如果JS文件是angular2组元,它就会查找HTML模板并创建包含模拟类组元为每个孩子一个规范文件包含在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; 

对于这个工作,你需要2个模板文件,一个用于组件,另一个用于注射服务。您可以添加管道和其他类型的TS类

component.txt模板:

/** 
* 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'); 
       } 
      } 
     }); 
    }) 

}); 

这里是模拟组件通过扩展mockInportTemplace.txt引用的模板:

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

这里由注射用扩展名引用的模板:

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 thre上面的e文件需要在你的项目下src下的文件夹中引用为单元测试模板

一旦你在你的可视代码中创建了这个扩展,转到你想要产生单元测试的JS文件,按F1 ,然后键入UniteTestMe。确保没有已经创建的spec文件。

+0

你是否写过这个扩展项目?我已经尝试过了,但是它停止发送失败并且没有任何调试信息的消息, –

+0

我没有为这个扩展做更多的开发。我建议你进入扩展代码,看看它失败的地方。看到这篇文章:https://code.visualstudio.com/docs/extensions/debugging-extensions – Mehdi

+0

我解决了问题,我有ts文件的问题,现在我想添加Service Mocks一代。 –

相关问题