diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js
index 7b4ae67..6082f04 100644
--- a/lib/commands/bootstrap.js
+++ b/lib/commands/bootstrap.js
@@ -13,10 +13,9 @@ const exec = require( '../utils/exec' );
 module.exports = {
 	/**
 	 * @param {Object} data
-	 * @param {Object} data.parameters Additional arguments provided by the user.
 	 * @param {String} data.packageName Name of current package to process.
 	 * @param {Options} data.options The options object.
-	 * @param {Repository|null} data.repository
+	 * @param {Repository} data.repository
 	 * @returns {Promise}
 	 */
 	execute( data ) {
@@ -25,19 +24,24 @@ module.exports = {
 		return new Promise( ( resolve, reject ) => {
 			const destinationPath = path.join( data.options.packages, data.repository.directory );
 
+			let promise;
+
 			// Package is already cloned.
 			if ( fs.existsSync( destinationPath ) ) {
-				log.info( `Package "${ data.packageName }" is already cloned. Skipping.` );
+				log.info( `Package "${ data.packageName }" is already cloned.` );
 
-				return resolve( { logs: log.all() } );
-			}
+				promise = Promise.resolve();
+			} else {
+				const command = [
+					`git clone --progress ${ data.repository.url } ${ destinationPath }`,
+					`cd ${ destinationPath }`,
+					`git checkout --quiet ${ data.repository.branch }`
+				].join( ' && ' );
 
-			const command =
-				`git clone --progress ${ data.repository.url } ${ destinationPath } && ` +
-				`cd ${ destinationPath } && ` +
-				`git checkout --quiet ${ data.repository.branch }`;
+				promise = exec( command );
+			}
 
-			exec( command )
+			promise
 				.then( ( output ) => {
 					log.info( output );
 
@@ -47,14 +51,14 @@ module.exports = {
 
 					if ( data.options.recursive ) {
 						const packageJson = require( path.join( destinationPath, 'package.json' ) );
-						let packages = [];
+						const packages = [];
 
 						if ( packageJson.dependencies ) {
-							packages = packages.concat( Object.keys( packageJson.dependencies ) );
+							packages.push( ...Object.keys( packageJson.dependencies ) );
 						}
 
 						if ( packageJson.devDependencies ) {
-							packages = packages.concat( Object.keys( packageJson.devDependencies ) );
+							packages.push( ...Object.keys( packageJson.devDependencies ) );
 						}
 
 						commandOutput.packages = packages;
diff --git a/lib/commands/exec.js b/lib/commands/exec.js
index c464872..07dd630 100644
--- a/lib/commands/exec.js
+++ b/lib/commands/exec.js
@@ -11,20 +11,20 @@ const exec = require( '../utils/exec' );
 
 module.exports = {
 	/**
-	 * @param {Array.<String>} parameters Arguments that user provided calling the mgit.
+	 * @param {Array.<String>} args Arguments that user provided calling the mgit.
 	 */
-	beforeExecute( parameters ) {
-		if ( parameters.length === 1 ) {
+	beforeExecute( args ) {
+		if ( args.length === 1 ) {
 			throw new Error( 'Missing command to execute. Use: mgit exec [command-to-execute].' );
 		}
 	},
 
 	/**
 	 * @param {Object} data
-	 * @param {Object} data.parameters Additional arguments provided by the user.
+	 * @param {Object} data.arguments Arguments that user provided calling the mgit.
 	 * @param {String} data.packageName Name of current package to process.
 	 * @param {Options} data.options The options object.
-	 * @param {Repository|null} data.repository
+	 * @param {Repository} data.repository
 	 * @returns {Promise}
 	 */
 	execute( data ) {
@@ -42,7 +42,7 @@ module.exports = {
 
 			process.chdir( newCwd );
 
-			exec( data.parameters[ 0 ] )
+			exec( data.arguments[ 0 ] )
 				.then( ( stdout ) => {
 					process.chdir( data.options.cwd );
 
diff --git a/lib/commands/savehashes.js b/lib/commands/savehashes.js
index 0d1beed..8b24df0 100644
--- a/lib/commands/savehashes.js
+++ b/lib/commands/savehashes.js
@@ -28,7 +28,7 @@ module.exports = {
 						commit: commitHash
 					};
 
-					log.info( `Commit: ${ commitHash }.` );
+					log.info( `Commit: "${ commitHash }".` );
 
 					resolve( {
 						response: commandResponse,
@@ -44,7 +44,7 @@ module.exports = {
 
 		function getExecData( command ) {
 			return Object.assign( {}, data, {
-				parameters: [ command ]
+				arguments: [ command ]
 			} );
 		}
 	},
@@ -56,7 +56,7 @@ module.exports = {
 	 * @param {Set} commandResponses Results of executed command for each package.
 	 */
 	afterExecute( processedPackages, commandResponses ) {
-		const cwd = require( '../utils/getcwd.js' )();
+		const cwd = require( '../utils/getcwd' )();
 		const mgitJsonPath = path.join( cwd, 'mgit.json' );
 
 		updateJsonFile( mgitJsonPath, ( json ) => {
diff --git a/lib/commands/update.js b/lib/commands/update.js
index 95aa314..bc88da9 100644
--- a/lib/commands/update.js
+++ b/lib/commands/update.js
@@ -12,10 +12,9 @@ const chalk = require( 'chalk' );
 module.exports = {
 	/**
 	 * @param {Object} data
-	 * @param {Object} data.parameters Additional arguments provided by the user.
 	 * @param {String} data.packageName Name of current package to process.
 	 * @param {Options} data.options The options object.
-	 * @param {Repository|null} data.repository
+	 * @param {Repository} data.repository
 	 * @returns {Promise}
 	 */
 	execute( data ) {
@@ -33,7 +32,6 @@ module.exports = {
 				const bootstrapOptions = {
 					options: data.options,
 					packageName: data.packageName,
-					mgit: data.mgit,
 					repository: data.repository
 				};
 
@@ -62,7 +60,10 @@ module.exports = {
 					log.concat( response.logs );
 				} )
 				.then( () => {
-					return execCommand.execute( getExecData( `git checkout ${ data.repository.branch }` ) );
+					return execCommand.execute( getExecData( `git checkout ${ data.repository.branch }` ) )
+						.catch( ( response ) => {
+							throw new Error( response.logs.error[ 0 ].replace( /^error\: /, '' ) );
+						} );
 				} )
 				.then( ( response ) => {
 					log.concat( response.logs );
@@ -72,7 +73,7 @@ module.exports = {
 				} )
 				.then( ( response ) => {
 					const stdout = response.logs.info.join( '\n' ).trim();
-					const isOnBranchRegexp = /HEAD detached at [\w\d]+/;
+					const isOnBranchRegexp = /HEAD detached at+/;
 
 					// If on a detached commit, mgit must not pull the changes.
 					if ( isOnBranchRegexp.test( stdout ) ) {
@@ -81,7 +82,10 @@ module.exports = {
 						return resolve( { logs: log.all() } );
 					}
 
-					return execCommand.execute( getExecData( `git pull origin ${ data.repository.branch }` ) );
+					return execCommand.execute( getExecData( `git pull origin ${ data.repository.branch }` ) )
+						.catch( ( response ) => {
+							throw new Error( response.logs.error[ 0 ] );
+						} );
 				} )
 				.then( ( response ) => {
 					log.concat( response.logs );
@@ -97,7 +101,7 @@ module.exports = {
 
 		function getExecData( command ) {
 			return Object.assign( {}, data, {
-				parameters: [ command ]
+				arguments: [ command ]
 			} );
 		}
 	},
diff --git a/lib/index.js b/lib/index.js
index 1ae622c..4b6e831 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -11,10 +11,10 @@ const logDisplay = require( './utils/displaylog' );
 const getOptions = require( './utils/getoptions' );
 
 /**
- * @param {Array.<String>} parameters Arguments that the user provided.
+ * @param {Array.<String>} args Arguments that the user provided.
  * @param {Options} options The options object. It will be extended with the default options.
  */
-module.exports = function( parameters, options ) {
+module.exports = function( args, options ) {
 	const startTime = process.hrtime();
 	const forkPool = createForkPool( path.join( __dirname, 'utils', 'child-process.js' ) );
 
@@ -23,13 +23,13 @@ module.exports = function( parameters, options ) {
 	const resolver = require( options.resolverPath );
 
 	// Remove all dashes from command name.
-	parameters[ 0 ] = parameters[ 0 ].replace( /-/g, '' );
+	args[ 0 ] = args[ 0 ].replace( /-/g, '' );
 
-	const commandPath = path.join( __dirname, 'commands', parameters[ 0 ] );
+	const commandPath = path.join( __dirname, 'commands', args[ 0 ] );
 	const command = require( commandPath );
 
 	if ( typeof command.beforeExecute == 'function' ) {
-		command.beforeExecute( parameters );
+		command.beforeExecute( args );
 	}
 
 	const processedPackages = new Set();
@@ -52,7 +52,7 @@ module.exports = function( parameters, options ) {
 
 		const data = {
 			command: commandPath,
-			parameters: parameters.slice( 1 ),
+			arguments: args.slice( 1 ),
 			options,
 			packageName: packageName,
 			repository: resolver( packageName, options )
diff --git a/lib/utils/log.js b/lib/utils/log.js
index 5d427be..3b5d057 100644
--- a/lib/utils/log.js
+++ b/lib/utils/log.js
@@ -25,6 +25,10 @@ module.exports = function log() {
 		},
 
 		log( type, msg ) {
+			if ( !msg ) {
+				return;
+			}
+
 			msg = msg.trim();
 
 			if ( !msg ) {
diff --git a/package.json b/package.json
index 9fc6f23..dc13ccf 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "guppy-pre-commit": "^0.4.0",
     "istanbul": "^0.4.5",
     "mocha": "^3.2.0",
+    "mockery": "^2.0.0",
     "sinon": "^1.17.7"
   },
   "repository": {
diff --git a/tests/commands/bootstrap.js b/tests/commands/bootstrap.js
new file mode 100644
index 0000000..5fc6f06
--- /dev/null
+++ b/tests/commands/bootstrap.js
@@ -0,0 +1,156 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint mocha:true */
+
+'use strict';
+
+const fs = require( 'fs' );
+const path = require( 'path' );
+const sinon = require( 'sinon' );
+const mockery = require( 'mockery' );
+const expect = require( 'chai' ).expect;
+
+describe( 'commands/bootstrap', () => {
+	let bootstrapCommand, sandbox, stubs, data;
+
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+
+		mockery.enable( {
+			useCleanCache: true,
+			warnOnReplace: false,
+			warnOnUnregistered: false
+		} );
+
+		stubs = {
+			exec: sandbox.stub(),
+			fs: {
+				existsSync: sandbox.stub( fs, 'existsSync' )
+			},
+			path: {
+				join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) )
+			}
+		};
+
+		data = {
+			packageName: 'test-package',
+			options: {
+				cwd: __dirname,
+				packages: 'packages'
+			},
+			repository: {
+				directory: 'test-package',
+				url: 'git@github.com/organization/test-package.git',
+				branch: 'master'
+			}
+		};
+
+		mockery.registerMock( '../utils/exec', stubs.exec );
+
+		bootstrapCommand = require( '../../lib/commands/bootstrap' );
+	} );
+
+	afterEach( () => {
+		sandbox.restore();
+		mockery.disable();
+	} );
+
+	describe( 'execute()', () => {
+		it( 'rejects promise if something went wrong', () => {
+			const error = new Error( 'Unexpected error.' );
+
+			stubs.fs.existsSync.returns( false );
+			stubs.exec.returns( Promise.reject( error ) );
+
+			return bootstrapCommand.execute( data )
+				.then(
+					() => {
+						throw new Error( 'Supposed to be rejected.' );
+					},
+					( response ) => {
+						expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` );
+					}
+				);
+		} );
+
+		it( 'clones a repository if is not available', () => {
+			stubs.fs.existsSync.returns( false );
+			stubs.exec.returns( Promise.resolve( 'Git clone log.' ) );
+
+			return bootstrapCommand.execute( data )
+				.then( ( response ) => {
+					expect( stubs.exec.calledOnce ).to.equal( true );
+
+					const cloneCommand = stubs.exec.firstCall.args[ 0 ].split( ' && ' );
+
+					// Clone the repository.
+					expect( cloneCommand[ 0 ] ).to.equal( 'git clone --progress git@github.com/organization/test-package.git packages/test-package' );
+					// Change the directory to cloned package.
+					expect( cloneCommand[ 1 ] ).to.equal( 'cd packages/test-package' );
+					// And check out to proper branch.
+					expect( cloneCommand[ 2 ] ).to.equal( 'git checkout --quiet master' );
+
+					expect( response.logs.info[ 0 ] ).to.equal( 'Git clone log.' );
+				} );
+		} );
+
+		it( 'does not clone a repository if is available', () => {
+			stubs.fs.existsSync.returns( true );
+
+			return bootstrapCommand.execute( data )
+				.then( ( response ) => {
+					expect( stubs.exec.called ).to.equal( false );
+
+					expect( response.logs.info[ 0 ] ).to.equal( 'Package "test-package" is already cloned.' );
+				} );
+		} );
+
+		it( 'installs dependencies of cloned package', () => {
+			data.options.recursive = true;
+			data.options.packages = __dirname + '/../fixtures';
+			data.repository.directory = 'project-a';
+
+			stubs.fs.existsSync.returns( true );
+
+			return bootstrapCommand.execute( data )
+				.then( ( response ) => {
+					expect( response.packages ).is.an( 'array' );
+					expect( response.packages ).to.deep.equal( [ 'test-foo' ] );
+				} );
+		} );
+
+		it( 'installs devDependencies of cloned package', () => {
+			data.options.recursive = true;
+			data.options.packages = __dirname + '/../fixtures';
+			data.repository.directory = 'project-with-options-in-mgitjson';
+
+			stubs.fs.existsSync.returns( true );
+
+			return bootstrapCommand.execute( data )
+				.then( ( response ) => {
+					expect( response.packages ).is.an( 'array' );
+					expect( response.packages ).to.deep.equal( [ 'test-bar' ] );
+				} );
+		} );
+	} );
+
+	describe( 'afterExecute()', () => {
+		it( 'informs about number of processed packages', () => {
+			const consoleLog = sandbox.stub( console, 'log' );
+
+			const processedPackages = new Set();
+			processedPackages.add( 'package-1' );
+			processedPackages.add( 'package-2' );
+
+			bootstrapCommand.afterExecute( processedPackages );
+
+			expect( consoleLog.calledOnce ).to.equal( true );
+			expect( consoleLog.firstCall.args[ 0 ] ).to.match( /2 packages have been processed\./ );
+
+			consoleLog.restore();
+		} );
+	} );
+} );
diff --git a/tests/commands/exec.js b/tests/commands/exec.js
new file mode 100644
index 0000000..001bc70
--- /dev/null
+++ b/tests/commands/exec.js
@@ -0,0 +1,131 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint mocha:true */
+
+'use strict';
+
+const fs = require( 'fs' );
+const path = require( 'path' );
+const sinon = require( 'sinon' );
+const mockery = require( 'mockery' );
+const expect = require( 'chai' ).expect;
+
+describe( 'commands/exec', () => {
+	let execCommand, sandbox, stubs, data;
+
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+
+		mockery.enable( {
+			useCleanCache: true,
+			warnOnReplace: false,
+			warnOnUnregistered: false
+		} );
+
+		stubs = {
+			exec: sandbox.stub(),
+			fs: {
+				existsSync: sandbox.stub( fs, 'existsSync' )
+			},
+			path: {
+				join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) )
+			},
+			process: {
+				chdir: sandbox.stub( process, 'chdir' )
+			}
+		};
+
+		data = {
+			// `execute` is called without the "exec" command (`mgit exec first-cmd other-cmd` => [ 'first-cmd', 'other-cmd' ]).
+			arguments: [ 'pwd' ],
+			packageName: 'test-package',
+			options: {
+				cwd: __dirname,
+				packages: 'packages'
+			},
+			repository: {
+				directory: 'test-package'
+			}
+		};
+
+		mockery.registerMock( '../utils/exec', stubs.exec );
+
+		execCommand = require( '../../lib/commands/exec' );
+	} );
+
+	afterEach( () => {
+		sandbox.restore();
+		mockery.disable();
+	} );
+
+	describe( 'beforeExecute()', () => {
+		it( 'throws an error if command to execute is not specified', () => {
+			expect( () => {
+				// `beforeExecute` is called with full user's input (mgit exec [command-to-execute]).
+				execCommand.beforeExecute( [ 'exec' ] );
+			} ).to.throw( Error, 'Missing command to execute. Use: mgit exec [command-to-execute].' );
+		} );
+
+		it( 'does nothing if command is specified', () => {
+			expect( () => {
+				execCommand.beforeExecute( [ 'exec', 'pwd' ] );
+			} ).to.not.throw( Error );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'does not execute the command if package is not available', () => {
+			stubs.fs.existsSync.returns( false );
+
+			return execCommand.execute( data )
+				.then(
+					() => {
+						throw new Error( 'Supposed to be rejected.' );
+					},
+					( response ) => {
+						expect( stubs.path.join.calledOnce ).to.equal( true );
+
+						const err = 'Package "test-package" is not available. Run "mgit bootstrap" in order to download the package.';
+						expect( response.logs.error[ 0 ] ).to.equal( err );
+					}
+				);
+		} );
+
+		it( 'rejects promise if something went wrong', () => {
+			const error = new Error( 'Unexpected error.' );
+
+			stubs.fs.existsSync.returns( true );
+			stubs.exec.returns( Promise.reject( error ) );
+
+			return execCommand.execute( data )
+				.then(
+					() => {
+						throw new Error( 'Supposed to be rejected.' );
+					},
+					( response ) => {
+						expect( stubs.process.chdir.calledTwice ).to.equal( true );
+						expect( stubs.process.chdir.firstCall.args[ 0 ] ).to.equal( 'packages/test-package' );
+						expect( stubs.process.chdir.secondCall.args[ 0 ] ).to.equal( __dirname );
+						expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` );
+					}
+				);
+		} );
+
+		it( 'resolves promise if command has been executed', () => {
+			const pwd = '/packages/test-package';
+			stubs.fs.existsSync.returns( true );
+			stubs.exec.returns( Promise.resolve( pwd ) );
+
+			return execCommand.execute( data )
+				.then( ( response ) => {
+					expect( stubs.process.chdir.calledTwice ).to.equal( true );
+					expect( stubs.process.chdir.firstCall.args[ 0 ] ).to.equal( 'packages/test-package' );
+					expect( stubs.process.chdir.secondCall.args[ 0 ] ).to.equal( __dirname );
+					expect( response.logs.info[ 0 ] ).to.equal( pwd );
+				} );
+		} );
+	} );
+} );
diff --git a/tests/commands/savehashes.js b/tests/commands/savehashes.js
new file mode 100644
index 0000000..d68f417
--- /dev/null
+++ b/tests/commands/savehashes.js
@@ -0,0 +1,140 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint mocha:true */
+
+'use strict';
+
+const path = require( 'path' );
+const sinon = require( 'sinon' );
+const mockery = require( 'mockery' );
+const expect = require( 'chai' ).expect;
+
+describe( 'commands/savehashes', () => {
+	let saveHashesCommand, sandbox, stubs, data, mgitJsonPath, updateFunction;
+
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+
+		mockery.enable( {
+			useCleanCache: true,
+			warnOnReplace: false,
+			warnOnUnregistered: false
+		} );
+
+		stubs = {
+			execCommand: {
+				execute: sandbox.stub()
+			},
+			path: {
+				join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) )
+			}
+		};
+
+		data = {
+			packageName: 'test-package',
+		};
+
+		mockery.registerMock( './exec', stubs.execCommand );
+		mockery.registerMock( '../utils/updatejsonfile', ( pathToFile, callback ) => {
+			mgitJsonPath = pathToFile;
+			updateFunction = callback;
+		} );
+		mockery.registerMock( '../utils/getcwd', () => {
+			return __dirname;
+		} );
+
+		saveHashesCommand = require( '../../lib/commands/savehashes' );
+	} );
+
+	afterEach( () => {
+		sandbox.restore();
+		mockery.disable();
+	} );
+
+	describe( 'execute()', () => {
+		it( 'rejects promise if called command returned an error', () => {
+			const error = new Error( 'Unexpected error.' );
+
+			stubs.execCommand.execute.returns( Promise.reject( error ) );
+
+			return saveHashesCommand.execute( data )
+				.then(
+					() => {
+						throw new Error( 'Supposed to be rejected.' );
+					},
+					( response ) => {
+						expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` );
+					}
+				);
+		} );
+
+		it( 'resolves promise with last commit id', () => {
+			const execCommandResponse = {
+				logs: {
+					info: [ '584f34152d782cc2f26453e10b93c4a16ef01925' ]
+				}
+			};
+
+			stubs.execCommand.execute.returns( Promise.resolve( execCommandResponse ) );
+
+			return saveHashesCommand.execute( data )
+				.then( ( commandResponse ) => {
+					expect( stubs.execCommand.execute.calledOnce ).to.equal( true );
+					expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( {
+						packageName: data.packageName,
+						arguments: [ 'git rev-parse HEAD' ]
+					} );
+
+					expect( commandResponse.response ).to.deep.equal( {
+						packageName: data.packageName,
+						commit: '584f34152d782cc2f26453e10b93c4a16ef01925'
+					} );
+
+					expect( commandResponse.logs.info[ 0 ] ).to.equal( 'Commit: "584f34152d782cc2f26453e10b93c4a16ef01925".' );
+				} );
+		} );
+	} );
+
+	describe( 'afterExecute()', () => {
+		it( 'updates collected hashes in "mgit.json"', () => {
+			const processedPackages = new Set();
+			const commandResponses = new Set();
+
+			processedPackages.add( 'test-package' );
+			processedPackages.add( 'package-test' );
+
+			commandResponses.add( {
+				packageName: 'test-package',
+				commit: '584f34152d782cc2f26453e10b93c4a16ef01925'
+			} );
+			commandResponses.add( {
+				packageName: 'package-test',
+				commit: '52910fe61a4c39b01e35462f2cc287d25143f485'
+			} );
+
+			saveHashesCommand.afterExecute( processedPackages, commandResponses );
+
+			let json = {
+				dependencies: {
+					'test-package': 'organization/test-package',
+					'package-test': 'organization/package-test',
+					'other-package': 'organization/other-package'
+				}
+			};
+
+			expect( mgitJsonPath ).to.equal( __dirname + '/mgit.json' );
+			expect( updateFunction ).to.be.a( 'function' );
+
+			json = updateFunction( json );
+
+			expect( json.dependencies ).to.deep.equal( {
+				'test-package': 'organization/test-package#584f34152d782cc2f26453e10b93c4a16ef01925',
+				'package-test': 'organization/package-test#52910fe61a4c39b01e35462f2cc287d25143f485',
+				'other-package': 'organization/other-package'
+			} );
+		} );
+	} );
+} );
diff --git a/tests/commands/update.js b/tests/commands/update.js
new file mode 100644
index 0000000..b45539a
--- /dev/null
+++ b/tests/commands/update.js
@@ -0,0 +1,299 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint mocha:true */
+
+'use strict';
+
+const fs = require( 'fs' );
+const path = require( 'path' );
+const sinon = require( 'sinon' );
+const mockery = require( 'mockery' );
+const expect = require( 'chai' ).expect;
+
+describe( 'commands/update', () => {
+	let updateCommand, sandbox, stubs, data;
+
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+
+		mockery.enable( {
+			useCleanCache: true,
+			warnOnReplace: false,
+			warnOnUnregistered: false
+		} );
+
+		stubs = {
+			exec: sandbox.stub(),
+			fs: {
+				existsSync: sandbox.stub( fs, 'existsSync' )
+			},
+			path: {
+				join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) )
+			},
+			bootstrapCommand: {
+				execute: sandbox.stub()
+			},
+			execCommand: {
+				execute: sandbox.stub()
+			}
+		};
+
+		data = {
+			packageName: 'test-package',
+			options: {
+				cwd: __dirname,
+				packages: 'packages'
+			},
+			repository: {
+				directory: 'test-package',
+				url: 'git@github.com/organization/test-package.git',
+				branch: 'master'
+			}
+		};
+
+		mockery.registerMock( './exec', stubs.execCommand );
+		mockery.registerMock( './bootstrap', stubs.bootstrapCommand );
+
+		updateCommand = require( '../../lib/commands/update' );
+	} );
+
+	afterEach( () => {
+		sandbox.restore();
+		mockery.disable();
+	} );
+
+	describe( 'execute()', () => {
+		it( 'clones a package if is not available', () => {
+			stubs.fs.existsSync.returns( false );
+			stubs.bootstrapCommand.execute.returns( Promise.resolve( {
+				logs: getCommandLogs( 'Cloned.' )
+			} ) );
+
+			return updateCommand.execute( data )
+				.then( ( response ) => {
+					expect( response.logs.info ).to.deep.equal( [
+						'Package "test-package" was not found. Cloning...',
+						'Cloned.'
+					] );
+
+					expect( stubs.bootstrapCommand.execute.calledOnce ).to.equal( true );
+				} );
+		} );
+
+		it( 'resolves promise after pulling the changes', () => {
+			stubs.fs.existsSync.returns( true );
+
+			const exec = stubs.execCommand.execute;
+
+			exec.onCall( 0 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 1 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 2 ).returns( Promise.resolve( {
+				logs: getCommandLogs( 'Already on \'master\'.' )
+			} ) );
+
+			exec.onCall( 3 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '* master\n  remotes/origin/master' )
+			} ) );
+
+			exec.onCall( 4 ).returns( Promise.resolve( {
+				logs: getCommandLogs( 'Already up-to-date.' )
+			} ) );
+
+			return updateCommand.execute( data )
+				.then( ( response ) => {
+					expect( exec.getCall( 0 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git status -s' );
+					expect( exec.getCall( 1 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git fetch' );
+					expect( exec.getCall( 2 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git checkout master' );
+					expect( exec.getCall( 3 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git branch' );
+					expect( exec.getCall( 4 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git pull origin master' );
+
+					expect( response.logs.info ).to.deep.equal( [
+						'Already on \'master\'.',
+						'Already up-to-date.'
+					] );
+
+					expect( exec.callCount ).to.equal( 5 );
+				} );
+		} );
+
+		it( 'aborts if package has uncommitted changes', () => {
+			stubs.fs.existsSync.returns( true );
+
+			const exec = stubs.execCommand.execute;
+
+			exec.returns( Promise.resolve( {
+				logs: getCommandLogs( ' M first-file.js\n ?? second-file.js' )
+			} ) );
+
+			return updateCommand.execute( data )
+				.then(
+					() => {
+						throw new Error( 'Supposed to be rejected.' );
+					},
+					( response ) => {
+						const errMsg = 'Error: Package "test-package" has uncommitted changes. Aborted.';
+
+						expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( errMsg );
+						expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git status -s' );
+					}
+				);
+		} );
+
+		it( 'does not pull the changes if detached on a commit or a tag', () => {
+			stubs.fs.existsSync.returns( true );
+
+			data.repository.branch = '1a0ff0a2ee60549656177cd2a18b057764ec2146';
+
+			const exec = stubs.execCommand.execute;
+
+			exec.onCall( 0 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 1 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 2 ).returns( Promise.resolve( {
+				logs: getCommandLogs( 'Note: checking out \'1a0ff0a2ee60549656177cd2a18b057764ec2146\'.' )
+			} ) );
+
+			exec.onCall( 3 ).returns( Promise.resolve( {
+				logs: getCommandLogs( [
+					'* (HEAD detached at 1a0ff0a2ee60549656177cd2a18b057764ec2146)',
+					'  master',
+					'  remotes/origin/master'
+				].join( '\n' ) )
+			} ) );
+
+			return updateCommand.execute( data )
+				.then( ( response ) => {
+					expect( response.logs.info ).to.deep.equal( [
+						'Note: checking out \'1a0ff0a2ee60549656177cd2a18b057764ec2146\'.',
+						'Package "test-package" is on a detached commit.'
+					] );
+
+					expect( exec.callCount ).to.equal( 4 );
+				} );
+		} );
+
+		it( 'aborts if user wants to pull changes from non-existing branch', () => {
+			stubs.fs.existsSync.returns( true );
+
+			data.repository.branch = 'develop';
+
+			const exec = stubs.execCommand.execute;
+
+			exec.onCall( 0 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 1 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 2 ).returns( Promise.resolve( {
+				logs: getCommandLogs( 'Already on \'develop\'.' )
+			} ) );
+
+			exec.onCall( 3 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '* develop' )
+			} ) );
+
+			exec.onCall( 4 ).returns( Promise.reject( {
+				logs: getCommandLogs( 'fatal: Couldn\'t find remote ref develop', true )
+			} ) );
+
+			return updateCommand.execute( data )
+				.then(
+					() => {
+						throw new Error( 'Supposed to be rejected.' );
+					},
+					( response ) => {
+						expect( response.logs.info ).to.deep.equal( [
+							'Already on \'develop\'.'
+						] );
+
+						const errMsg = 'Error: fatal: Couldn\'t find remote ref develop';
+						expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( errMsg );
+
+						expect( exec.callCount ).to.equal( 5 );
+					}
+				);
+		} );
+
+		it( 'aborts if user wants to check out to non-existing branch', () => {
+			stubs.fs.existsSync.returns( true );
+
+			data.repository.branch = 'non-existing-branch';
+
+			const exec = stubs.execCommand.execute;
+
+			exec.onCall( 0 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 1 ).returns( Promise.resolve( {
+				logs: getCommandLogs( '' )
+			} ) );
+
+			exec.onCall( 2 ).returns( Promise.reject( {
+				logs: getCommandLogs( 'error: pathspec \'ggdfgd\' did not match any file(s) known to git.', true ),
+			} ) );
+
+			return updateCommand.execute( data )
+				.then(
+					() => {
+						throw new Error( 'Supposed to be rejected.' );
+					},
+					( response ) => {
+						const errMsg = 'Error: pathspec \'ggdfgd\' did not match any file(s) known to git.';
+						expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( errMsg );
+
+						expect( exec.callCount ).to.equal( 3 );
+					}
+				);
+		} );
+	} );
+
+	describe( 'afterExecute()', () => {
+		it( 'informs about number of processed packages', () => {
+			const consoleLog = sandbox.stub( console, 'log' );
+
+			const processedPackages = new Set();
+			processedPackages.add( 'package-1' );
+			processedPackages.add( 'package-2' );
+
+			updateCommand.afterExecute( processedPackages );
+
+			expect( consoleLog.calledOnce ).to.equal( true );
+			expect( consoleLog.firstCall.args[ 0 ] ).to.match( /2 packages have been processed\./ );
+
+			consoleLog.restore();
+		} );
+	} );
+
+	function getCommandLogs( msg, isError = false ) {
+		const logs = {
+			error: [],
+			info: []
+		};
+
+		if ( isError ) {
+			logs.error.push( msg );
+		} else {
+			logs.info.push( msg );
+		}
+
+		return logs;
+	}
+} );
diff --git a/tests/fixtures/project-a/package.json b/tests/fixtures/project-a/package.json
new file mode 100644
index 0000000..2293083
--- /dev/null
+++ b/tests/fixtures/project-a/package.json
@@ -0,0 +1,5 @@
+{
+	"dependencies": {
+		"test-foo": "organization/test-foo"
+	}
+}
diff --git a/tests/fixtures/project-with-options-in-mgitjson/package.json b/tests/fixtures/project-with-options-in-mgitjson/package.json
new file mode 100644
index 0000000..0ff9259
--- /dev/null
+++ b/tests/fixtures/project-with-options-in-mgitjson/package.json
@@ -0,0 +1,5 @@
+{
+	"devDependencies": {
+		"test-bar": "organization/test-bar"
+	}
+}