它已经有一段时间,因为我发布了这个问题。我开发了一个视觉代码扩展来帮助完成我想与您分享的任务。 这个扩展的目的不仅仅是创建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文件。
你是否写过这个扩展项目?我已经尝试过了,但是它停止发送失败并且没有任何调试信息的消息, –
我没有为这个扩展做更多的开发。我建议你进入扩展代码,看看它失败的地方。看到这篇文章:https://code.visualstudio.com/docs/extensions/debugging-extensions – Mehdi
我解决了问题,我有ts文件的问题,现在我想添加Service Mocks一代。 –