├── src ├── helpers │ ├── index.ts │ └── get-used-methods.ts ├── cli.ts ├── public-api.ts ├── model │ └── index.ts ├── default-dependency-handler.ts ├── main.ts ├── parse-source-file.ts └── generate-unit-test.ts ├── .npmignore ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── spec ├── fixtures │ ├── tsconfig.json │ ├── helpers │ │ ├── events.ts │ │ ├── config.ts │ │ ├── event-bus.service.ts │ │ └── logger.service.ts │ ├── test.module.ts │ ├── components │ │ ├── home-page │ │ │ ├── home-page.component.ts │ │ │ └── home-page.component.spec.expected.ts │ │ ├── login │ │ │ ├── login-form.component.ts │ │ │ ├── login-form.component.spec.expected.ts │ │ │ └── login-form.component.spec.expected.with-handlers.ts │ │ └── home-page-with-inject │ │ │ ├── home-page.component.ts │ │ │ └── home-page.component.spec.expected.ts │ ├── auth.service.ts │ ├── auth.service.with-double-quote.ts │ ├── auth.service.spec.expected.ts │ ├── auth.service.with-double-quote.spec.expected.ts │ └── dependency-handlers │ │ └── event-bus.service.handler.ts ├── support │ └── jasmine.json └── integration.spec.ts ├── assets ├── component-example-2.png └── component-example.png ├── .travis.yml ├── CHANGELOG.md ├── tsconfig.json ├── templates ├── class.ts.tpl └── component.ts.tpl ├── LICENSE.md ├── package.json └── README.md /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-used-methods'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | spec 4 | assets 5 | tsconfig.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib/ 3 | jasmine-unit-test-generator-*.tgz -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /spec/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { run } from './main'; 3 | 4 | run(process.argv.slice(2)); 5 | -------------------------------------------------------------------------------- /assets/component-example-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FDIM/jasmine-unit-test-generator/HEAD/assets/component-example-2.png -------------------------------------------------------------------------------- /assets/component-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FDIM/jasmine-unit-test-generator/HEAD/assets/component-example.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | node_js: 4 | - 20 5 | - 22 6 | install: 7 | - npm install 8 | script: 9 | - npm run test -------------------------------------------------------------------------------- /src/public-api.ts: -------------------------------------------------------------------------------- 1 | import dependencyHandler from './default-dependency-handler'; 2 | 3 | export * from './helpers'; 4 | export * from './model'; 5 | export { run } from './main'; 6 | export const defaultDependencyHandler = dependencyHandler; -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "./*[sS]pec.ts" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.ts" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": true 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixtures/helpers/events.ts: -------------------------------------------------------------------------------- 1 | export class LoginEvent { 2 | constructor(public profile: any) { } 3 | } 4 | 5 | export class LogoutEvent { 6 | constructor(public reason: string) { } 7 | } 8 | 9 | export class AuthChangeEvent { 10 | constructor(public type: string) { }; 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixtures/helpers/config.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from "@angular/core"; 2 | 3 | export interface AppConfig { 4 | env: string; 5 | } 6 | 7 | export type ENV = 'dev' | 'test'; 8 | 9 | export const CONFIG_TOKEN = new InjectionToken('AppConfig'); 10 | export const ENV_TOKEN = new InjectionToken('ENV'); 11 | -------------------------------------------------------------------------------- /spec/fixtures/test.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { LoginFormComponent } from './components/login/login-form.component'; 3 | import { HomePageComponent } from './components/home-page/home-page.component'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | LoginFormComponent, 8 | HomePageComponent 9 | ] 10 | }) 11 | export class TestModule { } 12 | -------------------------------------------------------------------------------- /spec/fixtures/helpers/event-bus.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class EventBusService { 6 | 7 | publish(event: T | string, options?: {}): void { } 8 | 9 | of(channel: (new (...args: any[]) => T) | string, priority: boolean = false): Observable { 10 | return new Observable(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/get-used-methods.ts: -------------------------------------------------------------------------------- 1 | 2 | export function getUsedMethods(sourceCode: string, variable: string) { 3 | const result: string[] = []; 4 | const regex = new RegExp(`${variable}[\\\s]*\\\.([a-zA-Z0-9]+)[\\\(<]`, 'g'); 5 | let matches: RegExpExecArray | null; 6 | 7 | while (matches = regex.exec(sourceCode)) { 8 | if (result.indexOf(matches[1]) === -1) { 9 | result.push(decodeURIComponent(matches[1])); 10 | } 11 | } 12 | return result; 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.4.0 4 | 5 | - Used methods lookup now works better by ignoring whitespace 6 | - `inject` function is now supported on top level properties for dependencies lookup 7 | 8 | ## v1.3.1 9 | 10 | - Loose versions of optional dependencies 11 | 12 | ## v1.2.1 13 | 14 | - #4: fix for Inject token value not being used in component test template 15 | - #2: fix for createSpyObj usage when no methods were used in a dependency 16 | 17 | ## v1.2.0 18 | 19 | Initial release -------------------------------------------------------------------------------- /spec/fixtures/helpers/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class LoggerService { 5 | 6 | constructor( 7 | @Inject('window') protected window: Window, 8 | ) { } 9 | 10 | // public api 11 | log = this.getLog('log'); 12 | info = this.getLog('info'); 13 | warn = this.getLog('warn'); 14 | error = this.getLog('error'); 15 | debug = this.getLog('debug'); 16 | 17 | protected getLog(type: string): ((message: any, ..._args: any[]) => void) { 18 | return this.window.console[type] || (() => { }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "./node_modules" 4 | ], 5 | "include": [ 6 | "./src/*" 7 | ], 8 | "compileOnSave": false, 9 | "compilerOptions": { 10 | "typeRoots": [ "./node_modules/@types/" ], 11 | "outDir": "lib", 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "declaration": true, 17 | "skipLibCheck": true, 18 | "target": "es2015", 19 | "module": "commonjs", 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "strict": true, 23 | "strictPropertyInitialization": false, 24 | "lib": [ 25 | "es2015", 26 | "dom" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/components/home-page/home-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { AppConfig, CONFIG_TOKEN } from '../../helpers/config'; 4 | 5 | @Component({ 6 | selector: 'app-name', 7 | template: ``, 8 | styles: [``] 9 | }) 10 | export class HomePageComponent implements OnInit { 11 | constructor( 12 | private router: Router, 13 | private route: ActivatedRoute, 14 | @Inject('window') private window: Window, 15 | @Inject(CONFIG_TOKEN) private config: AppConfig, 16 | ) { } 17 | 18 | ngOnInit(): void { 19 | this.route.paramMap.subscribe(params => { 20 | if (params.has('type') && params.get('type') === 'user') { 21 | this.router 22 | .navigate(['home', 'user']); 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/fixtures/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | import { LoggerService } from './helpers/logger.service'; 3 | import { EventBusService } from './helpers/event-bus.service'; 4 | import { AuthChangeEvent } from './helpers/events'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | 9 | constructor( 10 | @Inject('window') protected window: Window, 11 | private logger: LoggerService, 12 | private eventBusService: EventBusService 13 | ) { } 14 | 15 | login(form: { username: string, password: string }) { 16 | this.logger.debug('[LoginService]', 'user logged in'); 17 | this.eventBusService.publish(new AuthChangeEvent('login')); 18 | this.window.setInterval(() => { }, 0) 19 | } 20 | 21 | logout() { 22 | this.logger.debug('[LoginService]', 'user logged out'); 23 | this.eventBusService.publish(new AuthChangeEvent('logout')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spec/fixtures/auth.service.with-double-quote.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from "@angular/core"; 2 | import { LoggerService } from "./helpers/logger.service"; 3 | import { EventBusService } from "./helpers/event-bus.service"; 4 | import { AuthChangeEvent } from "./helpers/events"; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | 9 | constructor( 10 | @Inject("window") protected window: Window, 11 | private logger: LoggerService, 12 | private eventBusService: EventBusService 13 | ) { } 14 | 15 | login(form: { username: string, password: string }) { 16 | this.logger.debug("[LoginService]", "user logged in"); 17 | this.eventBusService.publish(new AuthChangeEvent("login")); 18 | this.window.setInterval(() => { }, 0) 19 | } 20 | 21 | logout() { 22 | this.logger.debug("[LoginService]", "user logged out"); 23 | this.eventBusService.publish(new AuthChangeEvent("logout")); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/class.ts.tpl: -------------------------------------------------------------------------------- 1 | import { <%=name %> } from <%=quoteSymbol %><%=path %><%=quoteSymbol %>;<% 2 | imports.forEach(function(value) { %> 3 | import { <%=value.names.join(', ') %> } from <%=value.path %>;<% }) %> 4 | 5 | describe(<%=quoteSymbol %><%=name %><%=quoteSymbol %>, () => { 6 | let <%=instanceVariableName %>: <%=name %>;<% 7 | declarations.forEach(function(dec) { %> 8 | let <%=dec.name %>: <%=dec.type %>;<% }) %> 9 | 10 | function create<%=templateType %>() { 11 | <%=instanceVariableName %> = new <%=name %>(<% 12 | dependencies.forEach(function(dep) { %> 13 | <%=dep.name%>,<% }) %> 14 | ); 15 | } 16 | 17 | beforeEach(() => {<% 18 | initializers.forEach(function(factory) { %> 19 | <%=(factory.name ? (factory.name + ' = ') : '') + factory.value %>;<% }) %> 20 | 21 | create<%=templateType %>(); 22 | }); 23 | 24 | it(<%=quoteSymbol %>should create<%=quoteSymbol %>, () => { 25 | expect(<%=instanceVariableName %>).toBeTruthy(); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /spec/fixtures/auth.service.spec.expected.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './auth.service'; 2 | import { LoggerService } from './helpers/logger.service'; 3 | import { EventBusService } from './helpers/event-bus.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | let fakeWindow: jasmine.SpyObj; 8 | let fakeLogger: jasmine.SpyObj; 9 | let fakeEventBusService: jasmine.SpyObj; 10 | 11 | function createService() { 12 | service = new AuthService( 13 | fakeWindow, 14 | fakeLogger, 15 | fakeEventBusService, 16 | ); 17 | } 18 | 19 | beforeEach(() => { 20 | fakeWindow = jasmine.createSpyObj('Window', ['setInterval']); 21 | fakeLogger = jasmine.createSpyObj('LoggerService', ['debug']); 22 | fakeEventBusService = jasmine.createSpyObj('EventBusService', ['publish']); 23 | 24 | createService(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(service).toBeTruthy(); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /spec/fixtures/auth.service.with-double-quote.spec.expected.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from "./auth.service.with-double-quote"; 2 | import { LoggerService } from "./helpers/logger.service"; 3 | import { EventBusService } from "./helpers/event-bus.service"; 4 | 5 | describe("AuthService", () => { 6 | let service: AuthService; 7 | let fakeWindow: jasmine.SpyObj; 8 | let fakeLogger: jasmine.SpyObj; 9 | let fakeEventBusService: jasmine.SpyObj; 10 | 11 | function createService() { 12 | service = new AuthService( 13 | fakeWindow, 14 | fakeLogger, 15 | fakeEventBusService, 16 | ); 17 | } 18 | 19 | beforeEach(() => { 20 | fakeWindow = jasmine.createSpyObj("Window", ["setInterval"]); 21 | fakeLogger = jasmine.createSpyObj("LoggerService", ["debug"]); 22 | fakeEventBusService = jasmine.createSpyObj("EventBusService", ["publish"]); 23 | 24 | createService(); 25 | }); 26 | 27 | it("should create", () => { 28 | expect(service).toBeTruthy(); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Domas Trijonis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/fixtures/components/login/login-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { DOCUMENT } from '@angular/common'; 3 | import { AuthService } from '../../auth.service'; 4 | import { EventBusService } from '../../helpers/event-bus.service'; 5 | import { LoginEvent, LogoutEvent } from '../../helpers/events'; 6 | import { merge } from 'rxjs'; 7 | 8 | @Component({ 9 | selector: 'app-login-form', 10 | template: ``, 11 | styles: [``] 12 | }) 13 | export class LoginFormComponent implements OnInit { 14 | form: { 15 | username: string, 16 | password: string 17 | }; 18 | 19 | constructor( 20 | private authService: AuthService, 21 | private eventBusService: EventBusService, 22 | @Inject(DOCUMENT) private document: Document, 23 | @Inject('window') private window: Window, 24 | ) { } 25 | 26 | ngOnInit(): void { 27 | 28 | merge( 29 | this.eventBusService.of(LoginEvent), 30 | this.eventBusService.of(LogoutEvent) 31 | ).subscribe(); 32 | 33 | this.document.querySelectorAll('div'); 34 | } 35 | 36 | login() { 37 | this.authService.login(this.form); 38 | } 39 | 40 | error(error: string) { 41 | this.window.alert(error); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ParsedClass { 3 | name: string; 4 | dependencies: ParsedClassDependency[]; 5 | } 6 | 7 | export interface ParsedClassDependency { 8 | name: string; 9 | type?: string; 10 | token?: string; 11 | } 12 | export interface ParsedImport { 13 | path: string; 14 | names: string[]; 15 | } 16 | 17 | export interface ParsedSourceFile { 18 | imports: ParsedImport[]; 19 | classes: ParsedClass[]; 20 | } 21 | 22 | export interface ClassOptions { 23 | declarations: { name: string, type: string }[]; 24 | initializers: { name?: string, value: string }[]; 25 | dependencies: { name: string, token: string }[]; 26 | imports: ParsedImport[]; 27 | } 28 | 29 | export interface TemplateOptions { 30 | instanceVariableName: string; 31 | templateType: string; 32 | templatePath: string; 33 | } 34 | 35 | export interface DependencyHandlerOptions { 36 | variableName: string; 37 | injectionToken?: string; 38 | sourceCode: string; 39 | allImports: ParsedImport[]; 40 | quoteSymbol: string; 41 | } 42 | export interface DependencyHandler { 43 | run(result: ClassOptions, dep: ParsedClassDependency, options: DependencyHandlerOptions): void 44 | 45 | test(dep: ParsedClassDependency): boolean; 46 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jasmine-unit-test-generator", 3 | "version": "1.4.0", 4 | "description": "Generate initial unit test with all class dependencies mocked. Can be extended with custom handlers to cover move advanced cases", 5 | "keywords": [ 6 | "jasmine", 7 | "unit test generator", 8 | "angular unit test" 9 | ], 10 | "main": "lib/public-api.js", 11 | "scripts": { 12 | "build:dev": "tsc --watch", 13 | "build": "tsc", 14 | "test": "jasmine --require=ts-node/register" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "ssh://git@github.com:FDIM/jasmine-unit-test-generator.git" 19 | }, 20 | "author": "FDIM", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/lodash": "^4.17.16", 24 | "@types/node": "^22.13.10", 25 | "@types/jasmine": "^5.1.7", 26 | "jasmine": "^5.6.0", 27 | "ts-node": "^10.9.2", 28 | "typescript": "*" 29 | }, 30 | "bin": { 31 | "jasmine-unit-test-generator": "./lib/cli.js" 32 | }, 33 | "dependencies": { 34 | "lodash": "^4.17.21" 35 | }, 36 | "peerDependencies": { 37 | "typescript": "*" 38 | }, 39 | "optionalDependencies": { 40 | "@angular/common": "*", 41 | "@angular/core": "*", 42 | "@angular/router": "*", 43 | "rxjs": "*" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/components/home-page-with-inject/home-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, OnInit, ProviderToken } from '@angular/core'; 2 | import { take } from 'rxjs/operators'; 3 | import { Router, ActivatedRoute } from '@angular/router'; 4 | import { CONFIG_TOKEN, ENV, ENV_TOKEN } from '../../helpers/config'; 5 | import { AuthService } from '../../auth.service'; 6 | 7 | @Component({ 8 | selector: 'app-name', 9 | template: ``, 10 | styles: [``] 11 | }) 12 | export class HomePageComponent implements OnInit { 13 | protected isEnabled = true; 14 | private router = inject(Router); 15 | private route = inject(ActivatedRoute); 16 | private document = inject('document' as any); 17 | private window = inject('window' as unknown as ProviderToken); 18 | private config = inject(CONFIG_TOKEN, { optional: true }); 19 | private env: ENV = inject(ENV_TOKEN); 20 | private authService: AuthService = inject(AuthService); 21 | 22 | ngOnInit(): void { 23 | this.route.paramMap 24 | .pipe(take(1) 25 | ).subscribe(params => { 26 | if (params.has('type') && params.get('type') === 'user') { 27 | this.router 28 | .navigate(['home', 'user']); 29 | } 30 | }); 31 | this.authService.login({ username: 'a', password: 'b' }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/default-dependency-handler.ts: -------------------------------------------------------------------------------- 1 | import { ParsedClassDependency, ClassOptions, DependencyHandler, DependencyHandlerOptions } from './model'; 2 | import { getUsedMethods } from './helpers'; 3 | 4 | export default { 5 | run(result: ClassOptions, dep: ParsedClassDependency, options: DependencyHandlerOptions) { 6 | const usedMethods = getUsedMethods(options.sourceCode, dep.name); 7 | 8 | result.declarations.push({ 9 | name: options.variableName, 10 | type: `jasmine.SpyObj<${dep.type || 'any'}>` 11 | }); 12 | 13 | if (usedMethods.length > 0) { 14 | result.initializers.push({ 15 | name: options.variableName, 16 | value: `jasmine.createSpyObj<${dep.type || 'any'}>(${options.quoteSymbol}${dep.type === 'any' || !dep.type ? dep.name : dep.type}${options.quoteSymbol}, [${usedMethods.map(m => (options.quoteSymbol + m + options.quoteSymbol)).join(`, `)}])` 17 | }); 18 | } else { 19 | result.initializers.push({ 20 | name: options.variableName, 21 | value: `{} as jasmine.SpyObj<${dep.type || 'any'}>` 22 | }); 23 | } 24 | 25 | result.dependencies.push({ 26 | name: options.variableName, 27 | token: options.injectionToken || 'no-token' 28 | }); 29 | }, 30 | 31 | test(_dep: ParsedClassDependency) { 32 | return true; 33 | } 34 | } as DependencyHandler; 35 | -------------------------------------------------------------------------------- /templates/component.ts.tpl: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from <%=quoteSymbol %>@angular/core/testing<%=quoteSymbol %>; 2 | import { <%=name %> } from <%=quoteSymbol %><%=path %><%=quoteSymbol %>;<% 3 | imports.forEach(function(value) { %> 4 | import { <%=value.names.join(', ') %> } from <%=value.path %>;<% }) %> 5 | 6 | describe(<%=quoteSymbol %><%=name %><%=quoteSymbol %>, () => { 7 | let <%=instanceVariableName %>: <%=name %>; 8 | let fixture: ComponentFixture<<%=name %>>;<% 9 | declarations.forEach(function(dec) { %> 10 | let <%=dec.name %>: <%=dec.type %>;<% }) %> 11 | 12 | beforeEach(waitForAsync(() => {<% 13 | initializers.forEach(function(factory) { %> 14 | <%=(factory.name ? (factory.name + ' = ') : '') + factory.value%>;<% }) %> 15 | 16 | TestBed.configureTestingModule({ 17 | declarations: [<%=name %>], 18 | providers: [<% 19 | dependencies.forEach(function(dep) { %> 20 | { provide: <%=dep.token %>, useFactory: () => <%=dep.name%> },<% }) %> 21 | ] 22 | }) 23 | .compileComponents(); 24 | })); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(<%=name %>); 28 | <%=instanceVariableName %> = fixture.componentInstance; 29 | }); 30 | 31 | it(<%=quoteSymbol %>should create<%=quoteSymbol %>, () => { 32 | expect(<%=instanceVariableName %>).toBeTruthy(); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync, readdirSync } from 'fs'; 2 | import * as ts from 'typescript'; 3 | import { parseSourceFile } from './parse-source-file'; 4 | import { generateUnitTest } from './generate-unit-test'; 5 | import defaultDependencyHandler from './default-dependency-handler'; 6 | import { DependencyHandler } from './model'; 7 | 8 | export function run(params: string[]) { 9 | if (!params.length) { 10 | // tslint:disable-next-line:no-console 11 | console.error('missing path argument'); 12 | process.exit(1); 13 | } 14 | 15 | if (params.length > 1 && params[0].indexOf('--require') === 0) { 16 | require(params[1]); 17 | params = params.slice(2); 18 | } 19 | 20 | const handlers: DependencyHandler[] = []; 21 | if (params.length > 1 && params[0].indexOf('--handlers') === 0) { 22 | const files = readdirSync(params[1]); 23 | files.forEach((file) => { 24 | const value = require(process.cwd() + '/' + params[1] + '/' + file); 25 | handlers.push(value.default || value); 26 | }); 27 | params = params.slice(2); 28 | } 29 | handlers.push(defaultDependencyHandler); 30 | 31 | const path = params[0]; 32 | 33 | const specPath = path.substring(0, path.length - 2) + 'spec.ts'; 34 | const sourceCode = readFileSync(path).toString(); 35 | 36 | const sourceFile = ts.createSourceFile( 37 | path, 38 | sourceCode, 39 | ts.ScriptTarget.Latest, 40 | /*setParentNodes */ true 41 | ); 42 | 43 | const input = parseSourceFile(sourceFile); 44 | const output = generateUnitTest(path, sourceCode, input, handlers); 45 | 46 | writeFileSync(specPath, output); 47 | } 48 | -------------------------------------------------------------------------------- /spec/fixtures/components/home-page/home-page.component.spec.expected.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HomePageComponent } from './home-page.component'; 3 | import { Router, ActivatedRoute } from '@angular/router'; 4 | import { AppConfig, CONFIG_TOKEN } from '../../helpers/config'; 5 | 6 | describe('HomePageComponent', () => { 7 | let component: HomePageComponent; 8 | let fixture: ComponentFixture; 9 | let fakeRouter: jasmine.SpyObj; 10 | let fakeRoute: jasmine.SpyObj; 11 | let fakeWindow: jasmine.SpyObj; 12 | let fakeConfig: jasmine.SpyObj; 13 | 14 | beforeEach(waitForAsync(() => { 15 | fakeRouter = jasmine.createSpyObj('Router', ['navigate']); 16 | fakeRoute = {} as jasmine.SpyObj; 17 | fakeWindow = {} as jasmine.SpyObj; 18 | fakeConfig = {} as jasmine.SpyObj; 19 | 20 | TestBed.configureTestingModule({ 21 | declarations: [HomePageComponent], 22 | providers: [ 23 | { provide: Router, useFactory: () => fakeRouter }, 24 | { provide: ActivatedRoute, useFactory: () => fakeRoute }, 25 | { provide: 'window', useFactory: () => fakeWindow }, 26 | { provide: CONFIG_TOKEN, useFactory: () => fakeConfig }, 27 | ] 28 | }) 29 | .compileComponents(); 30 | })); 31 | 32 | beforeEach(() => { 33 | fixture = TestBed.createComponent(HomePageComponent); 34 | component = fixture.componentInstance; 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | }, 41 | { 42 | "label": "test and watch selected spec file", 43 | "type": "shell", 44 | "command": "ng", 45 | "args": [ 46 | "test", 47 | "--include", 48 | "${relativeFile}" 49 | ], 50 | "group": "test" 51 | }, 52 | { 53 | "label": "generate unit test", 54 | "command": "node", 55 | "type": "shell", 56 | "args": [ 57 | "./node_modules/jasmine-unit-test-generator/lib/cli.js", 58 | "${relativeFile}" 59 | ], 60 | "group": "test" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /spec/fixtures/components/login/login-form.component.spec.expected.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { LoginFormComponent } from './login-form.component'; 3 | import { DOCUMENT } from '@angular/common'; 4 | import { AuthService } from '../../auth.service'; 5 | import { EventBusService } from '../../helpers/event-bus.service'; 6 | 7 | describe('LoginFormComponent', () => { 8 | let component: LoginFormComponent; 9 | let fixture: ComponentFixture; 10 | let fakeAuthService: jasmine.SpyObj; 11 | let fakeEventBusService: jasmine.SpyObj; 12 | let fakeDocument: jasmine.SpyObj; 13 | let fakeWindow: jasmine.SpyObj; 14 | 15 | beforeEach(waitForAsync(() => { 16 | fakeAuthService = jasmine.createSpyObj('AuthService', ['login']); 17 | fakeEventBusService = jasmine.createSpyObj('EventBusService', ['of']); 18 | fakeDocument = jasmine.createSpyObj('Document', ['querySelectorAll']); 19 | fakeWindow = jasmine.createSpyObj('Window', ['alert']); 20 | 21 | TestBed.configureTestingModule({ 22 | declarations: [LoginFormComponent], 23 | providers: [ 24 | { provide: AuthService, useFactory: () => fakeAuthService }, 25 | { provide: EventBusService, useFactory: () => fakeEventBusService }, 26 | { provide: DOCUMENT, useFactory: () => fakeDocument }, 27 | { provide: 'window', useFactory: () => fakeWindow }, 28 | ] 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(LoginFormComponent); 35 | component = fixture.componentInstance; 36 | }); 37 | 38 | it('should create', () => { 39 | expect(component).toBeTruthy(); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /spec/fixtures/components/home-page-with-inject/home-page.component.spec.expected.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HomePageComponent } from './home-page.component'; 3 | import { ProviderToken } from '@angular/core'; 4 | import { Router, ActivatedRoute } from '@angular/router'; 5 | import { CONFIG_TOKEN, ENV, ENV_TOKEN } from '../../helpers/config'; 6 | import { AuthService } from '../../auth.service'; 7 | 8 | describe('HomePageComponent', () => { 9 | let component: HomePageComponent; 10 | let fixture: ComponentFixture; 11 | let fakeRouter: jasmine.SpyObj; 12 | let fakeRoute: jasmine.SpyObj; 13 | let fakeDocument: jasmine.SpyObj; 14 | let fakeWindow: jasmine.SpyObj; 15 | let fakeConfig: jasmine.SpyObj ? T : unknown>; 16 | let fakeEnv: jasmine.SpyObj; 17 | let fakeAuthService: jasmine.SpyObj; 18 | 19 | beforeEach(waitForAsync(() => { 20 | fakeRouter = jasmine.createSpyObj('Router', ['navigate']); 21 | fakeRoute = {} as jasmine.SpyObj; 22 | fakeDocument = {} as jasmine.SpyObj; 23 | fakeWindow = {} as jasmine.SpyObj; 24 | fakeConfig = {} as jasmine.SpyObj ? T : unknown>; 25 | fakeEnv = {} as jasmine.SpyObj; 26 | fakeAuthService = jasmine.createSpyObj('AuthService', ['login']); 27 | 28 | TestBed.configureTestingModule({ 29 | declarations: [HomePageComponent], 30 | providers: [ 31 | { provide: Router, useFactory: () => fakeRouter }, 32 | { provide: ActivatedRoute, useFactory: () => fakeRoute }, 33 | { provide: 'document', useFactory: () => fakeDocument }, 34 | { provide: 'window', useFactory: () => fakeWindow }, 35 | { provide: CONFIG_TOKEN, useFactory: () => fakeConfig }, 36 | { provide: ENV_TOKEN, useFactory: () => fakeEnv }, 37 | { provide: AuthService, useFactory: () => fakeAuthService }, 38 | ] 39 | }) 40 | .compileComponents(); 41 | })); 42 | 43 | beforeEach(() => { 44 | fixture = TestBed.createComponent(HomePageComponent); 45 | component = fixture.componentInstance; 46 | }); 47 | 48 | it('should create', () => { 49 | expect(component).toBeTruthy(); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /spec/fixtures/components/login/login-form.component.spec.expected.with-handlers.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { LoginFormComponent } from './login-form.component'; 3 | import { DOCUMENT } from '@angular/common'; 4 | import { AuthService } from '../../auth.service'; 5 | import { EventBusService } from '../../helpers/event-bus.service'; 6 | import { LoginEvent, LogoutEvent } from '../../helpers/events'; 7 | import { ReplaySubject } from 'rxjs'; 8 | 9 | describe('LoginFormComponent', () => { 10 | let component: LoginFormComponent; 11 | let fixture: ComponentFixture; 12 | let fakeAuthService: jasmine.SpyObj; 13 | let fakeEventBusService: jasmine.SpyObj; 14 | let loginEventSubject: ReplaySubject; 15 | let logoutEventSubject: ReplaySubject; 16 | let fakeDocument: jasmine.SpyObj; 17 | let fakeWindow: jasmine.SpyObj; 18 | 19 | beforeEach(waitForAsync(() => { 20 | fakeAuthService = jasmine.createSpyObj('AuthService', ['login']); 21 | fakeEventBusService = jasmine.createSpyObj('EventBusService', ['of']); 22 | loginEventSubject = new ReplaySubject(1); 23 | logoutEventSubject = new ReplaySubject(1); 24 | fakeEventBusService.of.and.callFake((ev) => { 25 | if (ev === LoginEvent) { 26 | return loginEventSubject; 27 | } else if (ev === LogoutEvent) { 28 | return logoutEventSubject; 29 | } else { 30 | throw new Error('event:' + ev + ' not mocked'); 31 | } 32 | }); 33 | fakeDocument = jasmine.createSpyObj('Document', ['querySelectorAll']); 34 | fakeWindow = jasmine.createSpyObj('Window', ['alert']); 35 | 36 | TestBed.configureTestingModule({ 37 | declarations: [LoginFormComponent], 38 | providers: [ 39 | { provide: AuthService, useFactory: () => fakeAuthService }, 40 | { provide: EventBusService, useFactory: () => fakeEventBusService }, 41 | { provide: DOCUMENT, useFactory: () => fakeDocument }, 42 | { provide: 'window', useFactory: () => fakeWindow }, 43 | ] 44 | }) 45 | .compileComponents(); 46 | })); 47 | 48 | beforeEach(() => { 49 | fixture = TestBed.createComponent(LoginFormComponent); 50 | component = fixture.componentInstance; 51 | }); 52 | 53 | it('should create', () => { 54 | expect(component).toBeTruthy(); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /spec/fixtures/dependency-handlers/event-bus.service.handler.ts: -------------------------------------------------------------------------------- 1 | // in actual implementation replace public api reference with 'jasmine-unit-test-generator' 2 | import { ParsedClassDependency, ClassOptions, DependencyHandler, defaultDependencyHandler, DependencyHandlerOptions } from '../../../src/public-api'; 3 | 4 | export default { 5 | run(result: ClassOptions, dep: ParsedClassDependency, options: DependencyHandlerOptions) { 6 | 7 | defaultDependencyHandler.run(result, dep, options); 8 | 9 | const usedEvents = findUsedEvents(dep, options); 10 | 11 | if (usedEvents.length) { 12 | const eventNames: string[] = []; 13 | usedEvents.forEach((ev, index) => { 14 | const name = eventNames[index] = ev.substr(0, 1).toLowerCase() + ev.substr(1).replace(/<.*>/, '') + 'Subject'; 15 | 16 | result.declarations.push({ 17 | name, 18 | type: `ReplaySubject<${ev}>` 19 | }); 20 | 21 | result.initializers.push({ 22 | name, 23 | value: `new ReplaySubject<${ev}>(1)` 24 | }); 25 | 26 | // find import for this event 27 | const eventImportPath = options.allImports.find(v => v.names.includes(ev)); 28 | if (eventImportPath) { // should never be false 29 | result.imports.push({ 30 | names: [ev], 31 | path: eventImportPath.path 32 | }); 33 | } 34 | 35 | }); 36 | 37 | // these will be deduped 38 | result.imports.push({ 39 | names: ['ReplaySubject'], 40 | path: 'rxjs' 41 | }) 42 | 43 | // warning, strange formatting is intentional to create correct output! 44 | result.initializers.push({ 45 | value: 46 | `${options.variableName}.of.and.callFake((ev) => { 47 | ${usedEvents.map((ev, index) => { 48 | return ` ${index === 0 ? 'if' : '} else if'} (ev === ${ev}) { 49 | return ${eventNames[index]}; 50 | `; 51 | }).join('')} } else { 52 | throw new Error('event:' + ev + ' not mocked'); 53 | } 54 | })` 55 | }); 56 | } 57 | }, 58 | 59 | test(dep: ParsedClassDependency) { 60 | return dep.type === 'EventBusService'; 61 | } 62 | } as DependencyHandler; 63 | 64 | function findUsedEvents(dep: ParsedClassDependency, options: DependencyHandlerOptions): string[] { 65 | const result: string[] = []; 66 | const regex = new RegExp(`${dep.name}\.of\\\(([^)]*)\\\)`, 'g'); 67 | let matches: RegExpExecArray | null; 68 | 69 | while (matches = regex.exec(options.sourceCode)) { 70 | if (result.indexOf(matches[1]) === -1) { 71 | result.push(decodeURIComponent(matches[1])); 72 | } 73 | } 74 | return result; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jasmine unit test generator 2 | 3 | Automates creation of initial unit test files taking dependencies into account. 4 | 5 | Supported types: 6 | 7 | * component: [source](spec/fixtures/components/login-form.component.ts), [generated spec](spec/fixtures/components/login-form.component.spec.expected.ts), [generated spec with custom handler](spec/fixtures/components/login-form.component.spec.expected.with-handlers.ts) 8 | * directive 9 | * service: [source](spec/fixtures/auth.service.ts), [generated spec](spec/fixtures/auth.service.spec.expected.ts) 10 | * service (double quote): [source](spec/fixtures/auth.service.with-double-quote.ts), [generated spec](spec/fixtures/auth.service.with-double-quote.spec.expected.ts) 11 | * pipe 12 | * class file (may not be useful depending on use case) 13 | 14 | > `Constructor` and the use of `inject` function in top level properties is supported. 15 | 16 | 17 | ## Preview 18 | 19 | Basic input/output example: 20 | 21 | ![Basic](./assets/component-example.png) 22 | 23 | With custom event bus service dependency handler: 24 | 25 | ![With custom event service dependency handler](./assets/component-example-2.png) 26 | 27 | 28 | ## Usage 29 | 30 | ### Installation 31 | 32 | run `npm i jasmine-unit-test-generator` 33 | 34 | ### Basic 35 | 36 | run `jasmine-unit-test-generator ` 37 | 38 | ### With custom dependency handlers: 39 | 40 | run `jasmine-unit-test-generator --handlers ` 41 | 42 | ### With dependency handlers written in typescript: 43 | 44 | install `ts-node` 45 | 46 | run `jasmine-unit-test-generator --require ts-node/register --handlers ` 47 | 48 | At the moment argument order is important! 49 | 50 | And note that if you install generator globally, ts-node must be installed globally as well. Otherwise both need to be installed locally in the project. 51 | 52 | ## Dependency handlers 53 | 54 | You can extend formatting of resulting spec files for each dependency by making a dependency handler. See [default-dependency-handler.ts](./src/default-dependency-handler.ts) and [event-bus.service.handler.ts](./spec/fixtures/dependency-handlers/event-bus.service.handler.ts) 55 | 56 | It is possible to add extra declarations, initializers and dependencies. 57 | 58 | ## Development 59 | 60 | It's probably best to: 61 | 62 | * add an input file in `spec/fixtures` folder, e.g. test.ts 63 | * add expected output file, e.g. test.spec.expected.ts 64 | * link them in integration.spec.ts 65 | 66 | Alternavely, you can: 67 | 68 | * run `npm link` 69 | * run `npm run build:dev` 70 | * run `jasmine-unit-test-generator