It has been some time since I posted this question. I developed a visual code extension to help with this task, which I want to share with you. The point of this extension is not only to create a specification file, but also to create boiler room code for all the test cases you need to write. It also creates the mocks and injections you need to speed you up. he adds a test case that will fail if you have not completed all the tests. Feel free to remove it if it does not meet your needs. This was done for the Angular2 ES6 project, but you can upgrade it for typescript as you wish:
// description: This extension will create a specification file for the given js file. // if the js file is an Angular2 component, it then looks for the html template and creates a specification file containing the Mock componentenet class for each child element included in 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]+)(-[az]+){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;
To do this, you need 2 template files, one for components and one for injection services. You can add channels and other types of TS classes
template.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'); } } }); }) });
Here is the template for Mock Components referenced by the mockInportTemplace.txt extension:
@Component({ selector: 'SELECTORPLACEHOLDER', template: 'HTMLTEMPLATEPLACEHOLDER' }) export class MOCKNAMEPLACEHOLDER { //Add @Input() variables here if necessary ALLINPUTSPLACEHOLDER }
Here is the template referenced by the extension for injection:
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 should live inside your project under src in the folder referenced as unit template tags
Once you create this extension in your visual code, go to the JS file that you want to generate unit test, press F1 and enter UniteTestMe. make sure the specification file is not already created.