angular - Hur man genererar enhetstester för befintlig Angular2-app med Jasmine Karma
Jag gick med i ett team som utvecklar en Angular2-app som kräver att alla enhetstester görs med Jasmine-ramverket. Jag undrade om det finns ett verktyg som kan generera spec-filer för varje klass (typ av boilerplate-kod) genom att placera testfall baserat på tillgängliga metoder och/eller baserat på attribut som *ng-If i mallarna. Här är ett exempel på komponent 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
}
//...
}
Och genererar en spec-fil: 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',() => {
//
});
)
Svar #1
Det har gått ett tag sedan jag publicerade den här frågan. Jag har utvecklat ett Visual Code-tillägg för att hjälpa till med denna uppgift som jag vill dela med dig. Poängen med detta tillägg är inte bara att skapa spec-filen, det genererar också viss mallkod för alla testfall du behöver skriva. Det skapar också de Mockar och injektioner du behöver för att komma igång snabbare. Det lägger till ett testfall som kommer att misslyckas om du inte har implementerat alla tester. Känn dig fri att ta bort det om det inte passar dina behov. Detta gjordes för ett Angular2 ES6-projekt, men du kan uppdatera det för TypeScript som du vill.
// beskrivning: Denna tillägg kommer att skapa en spec-fil för en given js-fil./om js-filen är en Angular2-komponent, kommer den sedan att söka efter HTML-mallen och skapa en spec-fil som innehåller en Mock komponentklass för varje barn som ingår i 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;
För att detta ska fungera behöver du 2 mallfiler, en för komponenter och en för injicerbara tjänster. Du kan lägga till pipes och andra typer av TS-klasser
component.txt mall :
/**
* 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');
}
}
});
})
});
Här är en mall för Mock-komponenter som refereras av tillägget mockInportTemplace.txt:
@Component({
selector: 'SELECTORPLACEHOLDER',
template: 'HTMLTEMPLATEPLACEHOLDER'
})
export class MOCKNAMEPLACEHOLDER {
//Add @Input() variables here if necessary
ALLINPUTSPLACEHOLDER
}
Här är mallen som refereras av tillägget för injicerbara:
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');
}
}
});
})
});
De tre filer ovan behöver finnas i ditt projekt under src i en mapp som refereras som unit-test-templates
När du skapar denna tillägg i din Visual Code, gå till en JS-fil som du vill generera en enhetstest för, tryck på F1 och skriv UniteTestMe. Se till att det inte redan finns en spec-fil skapad.
Svar #2
Det finns en betald Visual Studio-tillägg som heter Simon test, vänligen hitta länken nedan https://marketplace.visualstudio.com/items?itemName=SimonTest.simontest Det ger alternativet att generera enhetstest med standardkod. Testperioden för detta tillägg är 30 dagar.
