2017-02-21 2 views
5

Я присоединился к команде, разрабатывающей приложение Angular2, которое требует всех модульных тестов, которые должны быть выполнены с помощью системы Jasmine. Мне было интересно, есть ли инструмент, способный генерировать файлы спецификаций для каждого класса (вид кода плиты котла), помещая тестовые примеры на основе доступных методов и/или на основе таких атрибутов, как * ng-If в шаблонах. Вот пример компонента a.component.jsКак сгенерировать модульные тесты для существующего приложения Angular2 с Jasmine Karma

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 
    } 

    //... 
} 

и генерирует спецификацию файла: 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

Это было некоторое время, так как я разместил этот вопрос. Я разработал визуальное расширение кода, чтобы помочь с этой задачей, которую хочу поделиться с вами. Точка этого расширения предназначена не только для создания спецификационного файла, но также генерирует код котельной плиты для всех тестовых случаев, которые вам нужно написать. Он также создает Mocks и инъекции, которые вам нужны, чтобы вы ускорялись. он добавляет тестовый пример, который потерпит неудачу, если вы не выполнили все тесты. Не стесняйтесь удалять его, если он не соответствует вашим потребностям. Это было сделано для проекта Angular2 ES6, но вы можете обновить его для машинописных текстов, как вы пожелаете:

// description: Это расширение создаст файл спецификации для данного js-файла. // если файл, ИС является angular2 componenet, он будет искать шаблон HTML и создать файл спецификаций, содержащий Пробный класс componenet для каждого ребенка включены в 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'); 
       } 
      } 
     }); 
    }) 

}); 

Вот шаблон для Mock компонентов, на который ссылается расширение 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. убедитесь, что уже не создан файл спецификаций.

+0

Вы написали этот проект расширения? Я попытался, но он остановился с сообщением о том, что он не прошел и без какой-либо информации об отладке, –

+0

Я не сделал больше разработок для этого расширения. Я бы посоветовал вам вставить код расширения и посмотреть, где он не работает. см. это сообщение: https://code.visualstudio.com/docs/extensions/debugging-extensions – Mehdi

+0

Я исправил проблемы, у меня были проблемы с ts-файлами, и прямо сейчас я хочу добавить в него генерации Service Mocks. –

Смежные вопросы