单元测试 – 如何使用Jasmine Karma为现有的Angular2应用程序生
发布时间:2020-12-17 18:10:06 所属栏目:安全 来源:网络整理
导读:我加入了一个开发Angular2应用程序的团队,该应用程序需要使用Jasmine框架完成所有单元测试. 我想知道是否有一个工具能够通过基于可用方法和/或基于诸如模板中的* ng-If之类的属性放置测试用例来为每个类生成规范文件(类似于锅炉板代码). 以下是组件a.compone
我加入了一个开发Angular2应用程序的团队,该应用程序需要使用Jasmine框架完成所有单元测试.
我想知道是否有一个工具能够通过基于可用方法和/或基于诸如模板中的* ng-If之类的属性放置测试用例来为每个类生成规范文件(类似于锅炉板代码). 以下是组件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 } //... } 并生成一个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,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',() => { // }); ) 解决方法
我发布这个问题已经有一段时间了.我已经开发了一个可视代码扩展来帮助完成我想与您分享的这项任务.
这个扩展的重点不仅仅是创建spec文件,它还为您需要编写的所有测试用例生成一些样板代码. 它还可以创建您需要的模拟和注射,以加快您的速度. 如果你没有实现所有测试,它会添加一个失败的测试用例.如果它不符合您的需要,可以随意删除它. 这是为Angular2 ES6项目完成的,但您可以根据需要更新typescript脚本: // description:此扩展将为给定的js文件创建spec文件. 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-templatescomponent.txt"; htmlFile = filePath.replace('.js','.html'); }else if(fileContentString.indexOf('@Injectable()') > 0){ pathToTemplate = worspacePath + "unit-test-templatesinjectableObject.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-templatesmockInportTemplace.txt"); let MockDeclarationTemplate = MockDeclarationTemplatebits.toString(); let inputList = ''; if(inputs && inputs.length){ inputs = inputs.filter(put => put !== 'hidden'); inputs = inputs.filter((item,self) =>{ return self.indexOf(item) == pos;//remove duplicate }); inputs.forEach(put =>{ inputList += '@Input() ' + put + ';rnt' }); } let declarations = ''; for(var i=0;i < componentsUsed.length; i++){ if(i != 0){ declarations += 'rn'; } 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(',''); } }else{ fileTemplate = fileTemplate.replace('MockComponentsPlaceHolder',''); fileTemplate = fileTemplate.replace(',''); } 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,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 = /((//.*$)|(/*[sS]*?*/))/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+"',()=>{rntListOfFunctionsTested.push('"+item+"');rnt//expect(fixture.componentInstance."+item+"("+fn_arguments+")).toBe('SomeValue');rn});"); } }); 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,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 = /((//.*$)|(/*[sS]*?*/))/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,()=>{rntListOfFunctionsTested.push('"+item+"');rnt//expect(objInstance."+item+"("+fn_arguments+")).toBe('SomeValue');rn});"); } }); 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 ',' to your tests'); } } }); }) }); 上面的三个文件需要在src下的项目中存在于一个引用为unit-test-templates的文件夹中 在可视代码中创建此扩展后,转到要为其生成单元测试的JS文件,按F1,然后键入UniteTestMe.确保没有已创建的spec文件. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |