Преглед на файлове

Fixed tests after changes made to dev-plugin-create task.

Szymon Kupś преди 10 години
родител
ревизия
7fd7bb230b
променени са 4 файла, в които са добавени 121 реда и са изтрити 251 реда
  1. 0 114
      dev/tasks/dev/utils/tools.js
  2. 35 40
      dev/tests/dev/dev-plugin-create.js
  3. 14 24
      dev/tests/dev/git.js
  4. 72 73
      dev/tests/dev/tools.js

+ 0 - 114
dev/tasks/dev/utils/tools.js

@@ -1,122 +1,8 @@
 'use strict';
 
-let dirtyFiles,
-	ignoreList;
-
 const dependencyRegExp = /^ckeditor5-/;
 
 module.exports = {
-	/**
-	 * Check if a task (including its optional target) is in the queue of tasks to be executed by Grunt.
-	 *
-	 * @param grunt {Object} The Grunt object.
-	 * @param task {String} The task name. May optionally include the target (e.g. 'task:target').
-	 * @returns {Boolean} "true" if the task is in the queue.
-	 */
-	checkTaskInQueue( grunt, task ) {
-		const cliTasks = grunt.cli.tasks;
-
-		// Check if the task has been called directly.
-		const isDirectCall = ( cliTasks.indexOf( task ) > -1 );
-		// Check if this is a "default" call and that the task is inside "default".
-		const isDefaultTask = ( cliTasks.indexOf( 'default' ) > -1 ) || !cliTasks.length;
-		// Hacking Grunt hard.
-		const isTaskInDefault = isDefaultTask && ( grunt.task._tasks.default.info.indexOf( '"' + task + '"' ) > -1 );
-
-		return isDirectCall || isTaskInDefault;
-	},
-
-	/**
-	 * Configures a multi-task and defines targets that are queued to be run by Grunt.
-	 *
-	 * @param grunt {Object} The Grunt object.
-	 * @param options {Object} A list of options for the method. See the jscs and jshint tasks for example.
-	 */
-	setupMultitaskConfig( grunt, options ) {
-		const task = options.task;
-		const taskConfig = {};
-		const config = taskConfig[ task ] = {
-			options: options.defaultOptions
-		};
-
-		// "all" is the default target to be used if others are not to be run.
-		const all = options.targets.all;
-		let isAll = true;
-
-		delete options.targets.all;
-
-		Object.getOwnPropertyNames( options.targets ).forEach( function( target ) {
-			if ( this.checkTaskInQueue( grunt, task + ':' + target ) ) {
-				config[ target ] = options.targets[ target ]();
-				isAll = false;
-			}
-		}, this );
-
-		if ( isAll ) {
-			config.all = all();
-		}
-
-		// Append .gitignore entries to the ignore list.
-		if ( options.addGitIgnore ) {
-			let ignoreProp = task + '.options.' + options.addGitIgnore;
-			let ignores = grunt.config.get( ignoreProp ) || [];
-
-			ignores = ignores.concat( this.getGitIgnore( grunt ) );
-			grunt.config.set( ignoreProp, ignores );
-		}
-
-		// Merge over configurations set in gruntfile.js.
-		grunt.config.merge( taskConfig );
-	},
-
-	/**
-	 * Gets the list of ignores from `.gitignore`.
-	 *
-	 * @param grunt {Object} The Grunt object.
-	 * @returns {String[]} The list of ignores.
-	 */
-	getGitIgnore( grunt ) {
-		if ( !ignoreList ) {
-			ignoreList = grunt.file.read( '.gitignore' );
-
-			ignoreList = ignoreList
-				// Remove comment lines.
-				.replace( /^#.*$/gm, '' )
-				// Transform into array.
-				.split( /\n+/ )
-				// Remove empty entries.
-				.filter( function( path ) {
-					return !!path;
-				} );
-		}
-
-		return ignoreList;
-	},
-
-	/**
-	 * Gets the list of files that are supposed to be included in the next Git commit.
-	 *
-	 * @returns {String[]} A list of file paths.
-	 */
-	getGitDirtyFiles() {
-		// Cache it, so it is executed only once when running multiple tasks.
-		if ( !dirtyFiles ) {
-			dirtyFiles = this
-				// Compare the state of index with HEAD.
-				.shExec( 'git diff-index --name-only HEAD' )
-				// Remove trailing /n to avoid an empty entry.
-				.replace( /\s*$/, '' )
-				// Transform into array.
-				.split( '\n' );
-
-			// If nothing is returned, the array will one one empty string only.
-			if ( dirtyFiles.length == 1 && !dirtyFiles[ 0 ] ) {
-				dirtyFiles = [];
-			}
-		}
-
-		return dirtyFiles;
-	},
 
 	/**
 	 * Executes a shell command.

+ 35 - 40
dev/tests/dev/dev-plugin-create.js

@@ -14,10 +14,11 @@ const tools = require( '../../tasks/dev/utils/tools' );
 const inquiries = require( '../../tasks/dev/utils/inquiries' );
 const git = require( '../../tasks/dev/utils/git' );
 const path = require( 'path' );
-const emptyFn = () => { };
-let spies;
 
-describe( 'dev-tasks', () => {
+describe( 'dev-plugin-create', () => {
+	const emptyFn = () => { };
+	let spies;
+
 	const mainRepositoryPath = '/path/to/repository';
 	const workspaceRoot = '..';
 	const workspacePath = path.join( mainRepositoryPath, workspaceRoot );
@@ -32,13 +33,12 @@ describe( 'dev-tasks', () => {
 		spies = {
 			linkDirectories: sinon.stub( tools, 'linkDirectories' ),
 			npmInstall: sinon.stub( tools, 'npmInstall' ),
-			installGitHooks: sinon.stub( tools, 'installGitHooks' ),
 			getPluginName: sinon.stub( inquiries, 'getPluginName' ).returns( new Promise( ( r ) => r( pluginName ) ) ),
 			getPluginVersion: sinon.stub( inquiries, 'getPluginVersion' ).returns( new Promise( ( r ) => r( pluginVersion ) ) ),
 			getPluginGitHubUrl: sinon.stub( inquiries, 'getPluginGitHubUrl' ).returns( new Promise( ( r ) => r( gitHubUrl ) ) ),
 			initializeRepository: sinon.stub( git, 'initializeRepository' ),
 			updateJSONFile: sinon.stub( tools, 'updateJSONFile' ),
-			copyTemplateFiles: sinon.stub( tools, 'copyTemplateFiles' ),
+			copy: sinon.stub( tools, 'copy' ),
 			initialCommit: sinon.stub( git, 'initialCommit' )
 		};
 	}
@@ -49,43 +49,38 @@ describe( 'dev-tasks', () => {
 		}
 	}
 
-	describe( 'dev-plugin-create', () => {
-		const pluginCreateTask = require( '../../tasks/dev/tasks/dev-plugin-create' );
-		const repositoryPath = path.join( workspacePath, pluginName );
+	const pluginCreateTask = require( '../../tasks/dev/tasks/dev-plugin-create' );
+	const repositoryPath = path.join( workspacePath, pluginName );
 
-		it( 'should exist', () => expect( pluginCreateTask ).to.be.a( 'function' ) );
+	it( 'should exist', () => expect( pluginCreateTask ).to.be.a( 'function' ) );
 
-		it( 'should create a plugin', () => {
-			return pluginCreateTask( mainRepositoryPath, workspaceRoot, emptyFn ).then( () => {
-				expect( spies.getPluginName.calledOnce ).to.equal( true );
-				expect( spies.getPluginVersion.calledOnce ).to.equal( true );
-				expect( spies.getPluginGitHubUrl.calledOnce ).to.equal( true );
-				expect( spies.initializeRepository.calledOnce ).to.equal( true );
-				expect( spies.initializeRepository.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.copyTemplateFiles.calledOnce ).to.equal( true );
-				expect( spies.copyTemplateFiles.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.updateJSONFile.calledTwice ).to.equal( true );
-				expect( spies.updateJSONFile.firstCall.args[ 0 ] ).to.equal( path.join( repositoryPath, 'package.json' ) );
-				let updateFn = spies.updateJSONFile.firstCall.args[ 1 ];
-				let json = updateFn( {} );
-				expect( json.name ).to.equal( pluginName );
-				expect( json.version ).to.equal( pluginVersion );
-				expect( spies.updateJSONFile.secondCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, 'package.json' ) );
-				updateFn = spies.updateJSONFile.secondCall.args[ 1 ];
-				json = updateFn( {} );
-				expect( json.dependencies ).to.be.an( 'object' );
-				expect( json.dependencies[ pluginName ] ).to.equal( gitHubUrl );
-				expect( spies.initialCommit.calledOnce ).to.equal( true );
-				expect( spies.initialCommit.firstCall.args[ 0 ] ).to.equal( pluginName );
-				expect( spies.initialCommit.firstCall.args[ 1 ] ).to.equal( repositoryPath );
-				expect( spies.linkDirectories.calledOnce ).to.equal( true );
-				expect( spies.linkDirectories.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.linkDirectories.firstCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', pluginName ) );
-				expect( spies.npmInstall.calledOnce ).to.equal( true );
-				expect( spies.npmInstall.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.installGitHooks.calledOnce ).to.equal( true );
-				expect( spies.installGitHooks.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-			} );
+	it( 'should create a plugin', () => {
+		return pluginCreateTask( mainRepositoryPath, workspaceRoot, emptyFn ).then( () => {
+			expect( spies.getPluginName.calledOnce ).to.equal( true );
+			expect( spies.getPluginVersion.calledOnce ).to.equal( true );
+			expect( spies.getPluginGitHubUrl.calledOnce ).to.equal( true );
+			expect( spies.initializeRepository.calledOnce ).to.equal( true );
+			expect( spies.initializeRepository.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+			expect( spies.copy.called ).to.equal( true );
+			expect( spies.updateJSONFile.calledTwice ).to.equal( true );
+			expect( spies.updateJSONFile.firstCall.args[ 0 ] ).to.equal( path.join( repositoryPath, 'package.json' ) );
+			let updateFn = spies.updateJSONFile.firstCall.args[ 1 ];
+			let json = updateFn( {} );
+			expect( json.name ).to.equal( pluginName );
+			expect( json.version ).to.equal( pluginVersion );
+			expect( spies.updateJSONFile.secondCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, 'package.json' ) );
+			updateFn = spies.updateJSONFile.secondCall.args[ 1 ];
+			json = updateFn( {} );
+			expect( json.dependencies ).to.be.an( 'object' );
+			expect( json.dependencies[ pluginName ] ).to.equal( gitHubUrl );
+			expect( spies.initialCommit.calledOnce ).to.equal( true );
+			expect( spies.initialCommit.firstCall.args[ 0 ] ).to.equal( pluginName );
+			expect( spies.initialCommit.firstCall.args[ 1 ] ).to.equal( repositoryPath );
+			expect( spies.linkDirectories.calledOnce ).to.equal( true );
+			expect( spies.linkDirectories.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+			expect( spies.linkDirectories.firstCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', pluginName ) );
+			expect( spies.npmInstall.calledOnce ).to.equal( true );
+			expect( spies.npmInstall.firstCall.args[ 0 ] ).to.equal( repositoryPath );
 		} );
 	} );
 } );

+ 14 - 24
dev/tests/dev/git.js

@@ -7,7 +7,7 @@
 
 'use strict';
 
-let toRestore;
+let sandbox;
 const git = require( '../../tasks/dev/utils/git' );
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
@@ -15,10 +15,12 @@ const tools = require( '../../tasks/dev/utils/tools' );
 const expect = chai.expect;
 
 describe( 'utils', () => {
-	beforeEach( () => toRestore = [] );
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+	} );
 
 	afterEach( () => {
-		toRestore.forEach( item => item.restore() );
+		sandbox.restore();
 	} );
 
 	describe( 'git', () => {
@@ -128,11 +130,10 @@ describe( 'utils', () => {
 			it( 'should be defined', () => expect( git.cloneRepository ).to.be.a( 'function' ) );
 
 			it( 'should call clone commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const workspacePath = '/path/to/workspace/';
 				const urlInfo = git.parseRepositoryUrl( 'git@github.com:ckeditor/ckeditor5-core#new-feature' );
 				const cloneCommands = `cd ${ workspacePath } && git clone ${ urlInfo.server + urlInfo.repository }`;
-				toRestore.push( shExecStub );
 
 				git.cloneRepository( urlInfo, workspacePath );
 
@@ -145,11 +146,10 @@ describe( 'utils', () => {
 			it( 'should be defined', () => expect( git.checkout ).to.be.a( 'function' ) );
 
 			it( 'should call checkout commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const branchName = 'branch-to-checkout';
 				const checkoutCommands = `cd ${ repositoryLocation } && git checkout ${ branchName }`;
-				toRestore.push( shExecStub );
 
 				git.checkout( repositoryLocation, branchName );
 
@@ -161,11 +161,10 @@ describe( 'utils', () => {
 		describe( 'pull', () => {
 			it( 'should be defined', () => expect( git.pull ).to.be.a( 'function' ) );
 			it( 'should call pull commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const branchName = 'branch-to-pull';
 				const pullCommands = `cd ${ repositoryLocation } && git pull origin ${ branchName }`;
-				toRestore.push( shExecStub );
 
 				git.pull( repositoryLocation, branchName );
 
@@ -177,16 +176,11 @@ describe( 'utils', () => {
 		describe( 'initializeRepository', () => {
 			it( 'should be defined', () => expect( git.initializeRepository ).to.be.a( 'function' ) );
 			it( 'should call initialize commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const initializeCommands = [
-					`git init ${ repositoryLocation }`,
-					`cd ${ repositoryLocation }`,
-					`git remote add boilerplate ${ git.BOILERPLATE_REPOSITORY }`,
-					`git fetch boilerplate ${ git.BOILERPLATE_BRANCH }`,
-					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
+					`git init ${ repositoryLocation }`
 				];
-				toRestore.push( shExecStub );
 
 				git.initializeRepository( repositoryLocation );
 
@@ -198,10 +192,9 @@ describe( 'utils', () => {
 		describe( 'getStatus', () => {
 			it( 'should be defined', () => expect( git.getStatus ).to.be.a( 'function' ) );
 			it( 'should call status command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const statusCommands = `cd ${ repositoryLocation } && git status --porcelain -sb`;
-				toRestore.push( shExecStub );
 
 				git.getStatus( repositoryLocation );
 
@@ -213,7 +206,7 @@ describe( 'utils', () => {
 		describe( 'updateBoilerplate', () => {
 			it( 'should be defined', () => expect( git.updateBoilerplate ).to.be.a( 'function' ) );
 			it( 'should fetch and merge boilerplate if remote already exists', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const updateCommands = [
 					`cd ${ repositoryLocation }`,
@@ -221,7 +214,6 @@ describe( 'utils', () => {
 					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
 				];
 				shExecStub.onCall( 0 ).returns( 'origin\nboilerplate' );
-				toRestore.push( shExecStub );
 
 				git.updateBoilerplate( repositoryLocation );
 
@@ -230,7 +222,7 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should add boilerplate remote if one not exists', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const addRemoteCommands = `cd ${ repositoryLocation } && git remote add boilerplate ${ git.BOILERPLATE_REPOSITORY }`;
 				const updateCommands = [
@@ -239,7 +231,6 @@ describe( 'utils', () => {
 					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
 				];
 				shExecStub.onCall( 0 ).returns( 'origin\nnew' );
-				toRestore.push( shExecStub );
 
 				git.updateBoilerplate( repositoryLocation );
 
@@ -252,7 +243,7 @@ describe( 'utils', () => {
 		describe( 'initialCommit', () => {
 			it( 'should be defined', () => expect( git.initialCommit ).to.be.a( 'function' ) );
 			it( 'should execute commit commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const pluginName = 'ckeditor5-plugin-name';
 				const repositoryPath = '/path/to/repo';
 				const commitCommands = [
@@ -260,7 +251,6 @@ describe( 'utils', () => {
 					`git add .`,
 					`git commit -m "Initial commit for ${ pluginName }."`
 				];
-				toRestore.push( shExecStub );
 
 				git.initialCommit( pluginName, repositoryPath );
 

+ 72 - 73
dev/tests/dev/tools.js

@@ -13,13 +13,15 @@ const expect = chai.expect;
 const tools = require( '../../tasks/dev/utils/tools' );
 const path = require( 'path' );
 const fs = require( 'fs' );
-let toRestore;
+let sandbox;
 
 describe( 'utils', () => {
-	beforeEach( () => toRestore = [] );
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+	} );
 
 	afterEach( () => {
-		toRestore.forEach( item => item.restore() );
+		sandbox.restore();
 	} );
 
 	describe( 'tools', () => {
@@ -28,8 +30,7 @@ describe( 'utils', () => {
 
 			it( 'should execute command', () => {
 				const sh = require( 'shelljs' );
-				const execStub = sinon.stub( sh, 'exec' ).returns( { code: 0 } );
-				toRestore.push( execStub );
+				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0 } );
 
 				tools.shExec( 'command' );
 
@@ -38,8 +39,7 @@ describe( 'utils', () => {
 
 			it( 'should throw error on unsuccessful call', () => {
 				const sh = require( 'shelljs' );
-				const execStub = sinon.stub( sh, 'exec' ).returns( { code: 1 } );
-				toRestore.push( execStub );
+				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 1 } );
 
 				expect( () => {
 					tools.shExec( 'command' );
@@ -52,11 +52,10 @@ describe( 'utils', () => {
 			it( 'should be defined', () => expect( tools.linkDirectories ).to.be.a( 'function' ) );
 
 			it( 'should link directories', () => {
-				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( false );
-				const symlinkStub = sinon.stub( fs, 'symlinkSync' );
+				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( false );
+				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const source = '/source/dir';
 				const destination = '/destination/dir';
-				toRestore.push( symlinkStub, isDirectoryStub );
 
 				tools.linkDirectories( source, destination );
 
@@ -67,12 +66,11 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should remove destination directory before linking', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
-				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( true );
-				const symlinkStub = sinon.stub( fs, 'symlinkSync' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
+				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( true );
+				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const source = '/source/dir';
 				const destination = '/destination/dir';
-				toRestore.push( symlinkStub, shExecStub, isDirectoryStub );
 
 				tools.linkDirectories( source, destination );
 
@@ -119,10 +117,9 @@ describe( 'utils', () => {
 			it( 'should get directories in specified path', () => {
 				const fs = require( 'fs' );
 				const directories = [ 'dir1', 'dir2', 'dir3' ];
-				const readdirSyncStub = sinon.stub( fs, 'readdirSync', () => directories );
-				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( true );
+				const readdirSyncStub = sandbox.stub( fs, 'readdirSync', () => directories );
+				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( true );
 				const dirPath = 'path';
-				toRestore.push( readdirSyncStub, isDirectoryStub );
 
 				tools.getDirectories( dirPath );
 
@@ -139,9 +136,8 @@ describe( 'utils', () => {
 
 			it( 'should return true if path points to directory', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isDirectory: () => true } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isDirectory: () => true } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isDirectory( path );
 
@@ -152,9 +148,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if path does not point to directory', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isDirectory: () => false } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isDirectory: () => false } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isDirectory( path );
 
@@ -165,9 +160,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if statSync method throws', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync' ).throws();
+				const statSyncStub = sandbox.stub( fs, 'statSync' ).throws();
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isDirectory( path );
 
@@ -182,9 +176,8 @@ describe( 'utils', () => {
 
 			it( 'should return true if path points to file', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isFile: () => true } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isFile: () => true } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isFile( path );
 
@@ -195,9 +188,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if path does not point to directory', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isFile: () => false } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isFile: () => false } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isFile( path );
 
@@ -208,9 +200,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if statSync method throws', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync' ).throws();
+				const statSyncStub = sandbox.stub( fs, 'statSync' ).throws();
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isFile( path );
 
@@ -226,8 +217,7 @@ describe( 'utils', () => {
 			it( 'should return only ckeditor5 directories', () => {
 				const workspacePath = '/workspace/path';
 				const sourceDirectories = [ 'tools', 'ckeditor5', 'ckeditor5-core', '.bin', 'ckeditor5-plugin-image' ];
-				const getDirectoriesStub = sinon.stub( tools, 'getDirectories', () => sourceDirectories );
-				toRestore.push( getDirectoriesStub );
+				sandbox.stub( tools, 'getDirectories', () => sourceDirectories );
 				const directories = tools.getCKE5Directories( workspacePath );
 
 				expect( directories.length ).equal( 2 );
@@ -241,10 +231,9 @@ describe( 'utils', () => {
 			it( 'should read, update and save JSON file', () => {
 				const path = 'path/to/file.json';
 				const fs = require( 'fs' );
-				const readFileStub = sinon.stub( fs, 'readFileSync', () => '{}' );
+				const readFileStub = sandbox.stub( fs, 'readFileSync', () => '{}' );
 				const modifiedJSON = { modified: true };
-				const writeFileStub = sinon.stub( fs, 'writeFileSync' );
-				toRestore.push( readFileStub, writeFileStub );
+				const writeFileStub = sandbox.stub( fs, 'writeFileSync' );
 
 				tools.updateJSONFile( path, () => {
 					return modifiedJSON;
@@ -261,11 +250,10 @@ describe( 'utils', () => {
 		describe( 'readPackageName', () => {
 			const modulePath = 'path/to/module';
 			it( 'should read package name from NPM module', () => {
-				const isFileStub = sinon.stub( tools, 'isFile' ).returns( true );
+				sandbox.stub( tools, 'isFile' ).returns( true );
 				const fs = require( 'fs' );
 				const name = 'module-name';
-				const readFileStub = sinon.stub( fs, 'readFileSync' ).returns( JSON.stringify( { name: name } ) );
-				toRestore.push( isFileStub, readFileStub );
+				sandbox.stub( fs, 'readFileSync' ).returns( JSON.stringify( { name: name } ) );
 
 				const result = tools.readPackageName( modulePath );
 
@@ -273,8 +261,7 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should return null if no package.json is found', () => {
-				const isFileStub = sinon.stub( tools, 'isFile' ).returns( false );
-				toRestore.push( isFileStub );
+				sandbox.stub( tools, 'isFile' ).returns( false );
 
 				const result = tools.readPackageName( modulePath );
 
@@ -282,10 +269,9 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should return null if no name in package.json is provided', () => {
-				const isFileStub = sinon.stub( tools, 'isFile' ).returns( true );
+				sandbox.stub( tools, 'isFile' ).returns( true );
 				const fs = require( 'fs' );
-				const readFileStub = sinon.stub( fs, 'readFileSync' ).returns( JSON.stringify( { } ) );
-				toRestore.push( isFileStub, readFileStub );
+				sandbox.stub( fs, 'readFileSync' ).returns( JSON.stringify( { } ) );
 
 				const result = tools.readPackageName( modulePath );
 
@@ -296,9 +282,8 @@ describe( 'utils', () => {
 		describe( 'npmInstall', () => {
 			it( 'should be defined', () => expect( tools.npmInstall ).to.be.a( 'function' ) );
 			it( 'should execute npm install command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const path = '/path/to/repository';
-				toRestore.push( shExecStub );
 
 				tools.npmInstall( path );
 
@@ -310,9 +295,8 @@ describe( 'utils', () => {
 		describe( 'npmUpdate', () => {
 			it( 'should be defined', () => expect( tools.npmUpdate ).to.be.a( 'function' ) );
 			it( 'should execute npm update command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const path = '/path/to/repository';
-				toRestore.push( shExecStub );
 
 				tools.npmUpdate( path );
 
@@ -324,10 +308,9 @@ describe( 'utils', () => {
 		describe( 'npmUninstall', () => {
 			it( 'should be defined', () => expect( tools.npmUninstall ).to.be.a( 'function' ) );
 			it( 'should execute npm uninstall command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const path = '/path/to/repository';
 				const moduleName = 'module-name';
-				toRestore.push( shExecStub );
 
 				tools.npmUninstall( path, moduleName );
 
@@ -339,9 +322,8 @@ describe( 'utils', () => {
 		describe( 'installGitHooks', () => {
 			it( 'should be defined', () => expect( tools.installGitHooks ).to.be.a( 'function' ) );
 			it( 'should execute grunt githooks command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const path = '/path/to/repository';
-				toRestore.push( shExecStub );
 
 				tools.installGitHooks( path );
 
@@ -350,20 +332,44 @@ describe( 'utils', () => {
 			} );
 		} );
 
-		describe( 'copyTemplateFiles', () => {
-			it( 'should be defined', () => expect( tools.copyTemplateFiles ).to.be.a( 'function' ) );
-			it( 'should copy template files', () => {
+		describe( 'copy', () => {
+			it( 'should be defined', () => expect( tools.copy ).to.be.a( 'function' ) );
+			it( 'should copy files', () => {
+				const fs = require( 'fs-extra' );
 				const path = require( 'path' );
-				const TEMPLATE_PATH = './dev/tasks/gulp/dev/templates';
-				const templatesPath = path.resolve( TEMPLATE_PATH );
-				const shExecStub = sinon.stub( tools, 'shExec' );
-				const repositoryPath = '/path/to/repository';
-				toRestore.push( shExecStub );
+				let destination = 'destination';
+				let file = 'file.js';
+				sandbox.stub( fs, 'ensureDirSync' );
+				const copyStub = sandbox.stub( fs, 'copySync' );
+				sandbox.stub( tools, 'isFile', () => true );
+				sandbox.stub( tools, 'isDirectory', () => false );
 
-				tools.copyTemplateFiles( repositoryPath );
+				tools.copy( [ file ], destination );
 
-				expect( shExecStub.calledOnce ).to.equal( true );
-				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cp ${ path.join( templatesPath, '*.md' ) } ${ repositoryPath }` );
+				file = path.resolve( file );
+				destination = path.join( path.resolve( destination ), path.basename( file ) );
+
+				sinon.assert.calledOnce( copyStub );
+				sinon.assert.calledWithExactly( copyStub.firstCall, file, destination );
+			} );
+
+			it( 'should copy directories', () => {
+				const fs = require( 'fs-extra' );
+				const path = require( 'path' );
+				let destination = 'destination';
+				let dir = 'source';
+				sandbox.stub( fs, 'ensureDirSync' );
+				const copyStub = sandbox.stub( fs, 'copySync' );
+				sandbox.stub( tools, 'isFile', () => false );
+				sandbox.stub( tools, 'isDirectory', () => true );
+
+				tools.copy( [ dir ], destination );
+
+				dir = path.resolve( dir );
+				destination = path.resolve( destination );
+
+				sinon.assert.calledOnce( copyStub );
+				sinon.assert.calledWithExactly( copyStub.firstCall, dir, destination );
 			} );
 		} );
 
@@ -376,10 +382,9 @@ describe( 'utils', () => {
 
 			it( 'should be defined', () => expect( tools.getGitUrlFromNpm ).to.be.a( 'function' ) );
 			it( 'should call npm view command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec', () => {
+				const shExecStub = sandbox.stub( tools, 'shExec', () => {
 					return JSON.stringify( repository );
 				} );
-				toRestore.push( shExecStub );
 				const url = tools.getGitUrlFromNpm( moduleName );
 
 				expect( shExecStub.calledOnce ).to.equal( true );
@@ -388,27 +393,21 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should return null if module is not found', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' ).throws( new Error( 'npm ERR! code E404' ) );
-				toRestore.push( shExecStub );
-
+				sandbox.stub( tools, 'shExec' ).throws( new Error( 'npm ERR! code E404' ) );
 				const url = tools.getGitUrlFromNpm( moduleName );
 				expect( url ).to.equal( null );
 			} );
 
 			it( 'should return null if module has no repository information', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' ).returns( JSON.stringify( {} ) );
-				toRestore.push( shExecStub );
-
+				sandbox.stub( tools, 'shExec' ).returns( JSON.stringify( {} ) );
 				const url = tools.getGitUrlFromNpm( moduleName );
 				expect( url ).to.equal( null );
 			} );
 
 			it( 'should throw on other errors', () => {
 				const error = new Error( 'Random error.' );
-				const shExecStub = sinon.stub( tools, 'shExec' ).throws( error );
-				const getUrlSpy = sinon.spy( tools, 'getGitUrlFromNpm' );
-				toRestore.push( shExecStub );
-				toRestore.push( getUrlSpy );
+				sandbox.stub( tools, 'shExec' ).throws( error );
+				const getUrlSpy = sandbox.spy( tools, 'getGitUrlFromNpm' );
 
 				try {
 					tools.getGitUrlFromNpm( moduleName );