Repository: wix/Detox
Pull request #2060 (2020-05-13 12:44:25)

Introduce some technical refactoring to android drivers layer
- [ ] This is a small change 
- [ ] This change has been discussed in issue #<?> and the solution has been agreed upon with maintainers.

---

**Description:**
Applying some of the work done in the context of #2058 - which in itself, will not be merged after all, it appears.


Patch:
diff --git a/detox/src/devices/drivers/android/AndroidDriver.js b/detox/src/devices/drivers/android/AndroidDriver.js
index a3b8909c6..d7b0b7f96 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -24,7 +24,7 @@ const temporaryPath = require('../../../artifacts/utils/temporaryPath');
 const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const sleep = require('../../../utils/sleep');
 const retry = require('../../../utils/retry');
-const { interruptProcess, spawnAndLog } = require('../../../utils/exec');
+const { interruptProcess } = require('../../../utils/exec');
 const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
 const AndroidExpect = require('../../../android/expect');
 const { InstrumentationLogsParser } = require('./InstrumentationLogsParser');
@@ -264,10 +264,10 @@ class AndroidDriver extends DeviceDriverBase {
     const serverPort = new URL(this.client.configuration.server).port;
     await this.adb.reverse(deviceId, serverPort);
     const testRunner = await this.adb.getInstrumentationRunner(deviceId, bundleId);
-    const spawnFlags = [`-s`, `${deviceId}`, `shell`, `am`, `instrument`, `-w`, `-r`, ...launchArgs, ...additionalLaunchArgs, testRunner];
+    const spawnFlags = [...launchArgs, ...additionalLaunchArgs];
 
     this.instrumentationLogsParser = new InstrumentationLogsParser();
-    this.instrumentationProcess = spawnAndLog(this.adb.adbBin, spawnFlags, { detached: false });
+    this.instrumentationProcess = this.adb.spawnInstrumentation(deviceId, spawnFlags, testRunner);
     this.instrumentationProcess.childProcess.stdout.setEncoding('utf8');
     this.instrumentationProcess.childProcess.stdout.on('data', this._extractStackTraceFromInstrumLogs.bind(this));
     this.instrumentationProcess.childProcess.on('close', async () => {
diff --git a/detox/src/devices/drivers/android/EmulatorDriver.js b/detox/src/devices/drivers/android/EmulatorDriver.js
index debaf43d0..b23367882 100644
--- a/detox/src/devices/drivers/android/EmulatorDriver.js
+++ b/detox/src/devices/drivers/android/EmulatorDriver.js
@@ -22,7 +22,7 @@ const DetoxEmulatorsPortRange = {
   max: 20000
 };
 
-const ACQUIRE_DEVICE_EV = 'ACQUIRE_DEVICE';
+const ALLOCATE_DEVICE_LOG_EVT = 'ALLOCATE_DEVICE';
 const EMU_BIN_STABLE_SKIN_VER = 28;
 
 class EmulatorDriver extends AndroidDriver {
@@ -153,9 +153,9 @@ class EmulatorDriver extends AndroidDriver {
   }
 
   async _allocateDevice(avdName) {
-    log.debug({ event: ACQUIRE_DEVICE_EV }, `Looking up a device based on ${avdName}`);
+    log.debug({ event: ALLOCATE_DEVICE_LOG_EVT }, `Trying to allocate a device based on ${avdName}`);
     const adbName = await this.deviceRegistry.allocateDevice(() => this._doAllocateDevice(avdName));
-    log.debug({ event: ACQUIRE_DEVICE_EV }, `Settled on ${adbName}`);
+    log.debug({ event: ALLOCATE_DEVICE_LOG_EVT }, `Settled on ${adbName}`);
     return adbName;
   }
 
diff --git a/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.js b/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.js
index 2ad8e4f96..ac550dde9 100644
--- a/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.js
+++ b/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.js
@@ -1,33 +1,31 @@
 const AdbDevicesHelper = require('../tools/AdbDevicesHelper');
 const log = require('../../../../utils/logger').child({ __filename });
 
-const ACQUIRE_DEVICE_EV = 'ACQUIRE_DEVICE';
+const DEVICE_LOOKUP_LOG_EVT = 'DEVICE_LOOKUP';
 
 class FreeEmulatorFinder {
   constructor(adb, deviceRegistry, avdName) {
-    this.adbDevicesHelper = new AdbDevicesHelper(adb);
-    this.deviceRegistry = deviceRegistry;
-    this.avdName = avdName;
+    this._adbDevicesHelper = new AdbDevicesHelper(adb);
+    this._deviceRegistry = deviceRegistry;
+    this._avdName = avdName;
 
     this._matcherFn = this._matcherFn.bind(this);
   }
 
   async findFreeDevice() {
-    return await this.adbDevicesHelper.lookupDevice(this._matcherFn);
+    return await this._adbDevicesHelper.lookupDevice(this._matcherFn);
   }
 
   async _matcherFn(candidate) {
     const isEmulator = candidate.type === 'emulator';
-    const isBusy = this.deviceRegistry.isDeviceBusy(candidate.adbName);
+    const isBusy = this._deviceRegistry.isDeviceBusy(candidate.adbName);
 
     if (isEmulator && !isBusy) {
-      if (await candidate.queryName() === this.avdName) {
-        log.debug({ event: ACQUIRE_DEVICE_EV }, `Found ${candidate.adbName}!`);
+      if (await candidate.queryName() === this._avdName) {
+        log.debug({ event: DEVICE_LOOKUP_LOG_EVT }, `Found ${candidate.adbName}!`);
         return true;
       }
-      log.debug({ event: ACQUIRE_DEVICE_EV }, `${candidate.adbName} is available but AVD is different`);
-    } else {
-      log.debug({ event: ACQUIRE_DEVICE_EV }, `${candidate.adbName} is not a free emulator`, isEmulator, !isBusy);
+      log.debug({ event: DEVICE_LOOKUP_LOG_EVT }, `${candidate.adbName} is available but AVD is different`);
     }
     return false;
   }
diff --git a/detox/src/devices/drivers/android/tools/ADB.js b/detox/src/devices/drivers/android/tools/ADB.js
index fbfda0ed3..c099d9513 100644
--- a/detox/src/devices/drivers/android/tools/ADB.js
+++ b/detox/src/devices/drivers/android/tools/ADB.js
@@ -232,6 +232,14 @@ class ADB {
     await this.shell(deviceId, `rm ${force ? '-f' : ''} "${path}"`);
   }
 
+  /***
+   * @returns {ChildProcessPromise}
+   */
+  spawnInstrumentation(deviceId, userArgs, testRunner) {
+    const spawnArgs = ['shell', 'am', 'instrument', '-w', '-r', ...userArgs, testRunner];
+    return this.spawn(deviceId, spawnArgs, { detached: false });
+  }
+
   async listInstrumentation(deviceId) {
     return this.shell(deviceId, 'pm list instrumentation');
   }
@@ -263,6 +271,7 @@ class ADB {
     return this.adbCmd(deviceId, `reverse --remove tcp:${port}`);
   }
 
+  // TODO refactor the whole thing so as to make usage of BinaryExec -- similar to EmulatorExec
   async adbCmd(deviceId, params, options) {
     const serial = `${deviceId ? `-s ${deviceId}` : ''}`;
     const cmd = `"${this.adbBin}" ${serial} ${params}`;
@@ -275,9 +284,9 @@ class ADB {
   /***
    * @returns {ChildProcessPromise}
    */
-  spawn(deviceId, params) {
+  spawn(deviceId, params, spawnOptions) {
     const serial = deviceId ? ['-s', deviceId] : [];
-    return spawnAndLog(this.adbBin, [...serial, ...params]);
+    return spawnAndLog(this.adbBin, [...serial, ...params], spawnOptions);
   }
 
   static inferDeviceType(adbName) {
diff --git a/detox/src/devices/drivers/android/tools/BinaryExec.js b/detox/src/devices/drivers/android/tools/BinaryExec.js
new file mode 100644
index 000000000..668ede02c
--- /dev/null
+++ b/detox/src/devices/drivers/android/tools/BinaryExec.js
@@ -0,0 +1,39 @@
+const spawn = require('child-process-promise').spawn;
+const exec = require('../../../../utils/exec').execWithRetriesAndLogs;
+
+class ExecCommand {
+  toString() {
+    return this._getArgsString();
+  }
+
+  _getArgs() {
+    return [];
+  }
+
+  _getArgsString() {
+    return this._getArgs().join(' ');
+  }
+}
+
+class BinaryExec {
+  constructor(binary) {
+    this.binary = binary;
+  }
+
+  toString() {
+    return this.binary;
+  }
+
+  async exec(command) {
+    return (await exec(`"${this.binary}" ${command._getArgsString()}`)).stdout;
+  }
+
+  spawn(command, stdout, stderr) {
+    return spawn(this.binary, command._getArgs(), { detached: true, stdio: ['ignore', stdout, stderr] });
+  }
+}
+
+module.exports = {
+  ExecCommand,
+  BinaryExec,
+};
diff --git a/detox/src/devices/drivers/android/tools/EmulatorExec.js b/detox/src/devices/drivers/android/tools/EmulatorExec.js
index 316315dc3..37c785ac1 100644
--- a/detox/src/devices/drivers/android/tools/EmulatorExec.js
+++ b/detox/src/devices/drivers/android/tools/EmulatorExec.js
@@ -1,52 +1,31 @@
 const _ = require('lodash');
 const os = require('os');
-const spawn = require('child-process-promise').spawn;
-const exec = require('../../../../utils/exec').execWithRetriesAndLogs;
 const argparse = require('../../../../utils/argparse');
-const {getAndroidEmulatorPath} = require('../../../../utils/environment');
+const { getAndroidEmulatorPath } = require('../../../../utils/environment');
+const {
+  ExecCommand,
+  BinaryExec,
+} = require('./BinaryExec');
 
-class EmulatorExec {
+class EmulatorExec extends BinaryExec {
   constructor() {
-    this.binary = getAndroidEmulatorPath();
-  }
-
-  toString() {
-    return this.binary;
-  }
-
-  async exec(command) {
-    return (await exec(`"${this.binary}" ${command._getArgsString()}`)).stdout;
-  }
-
-  spawn(command, stdout, stderr) {
-    return spawn(this.binary, command._getArgs(), { detached: true, stdio: ['ignore', stdout, stderr] });
-  }
-}
-
-class EmulatorCommand {
-  toString() {
-    return this._getArgsString();
-  }
-
-  _getArgs() {}
-  _getArgsString() {
-    return this._getArgs().join(' ');
+    super(getAndroidEmulatorPath());
   }
 }
 
-class ListAVDsCommand extends EmulatorCommand {
+class ListAVDsCommand extends ExecCommand {
   _getArgs() {
     return ['-list-avds', '--verbose'];
   }
 }
 
-class QueryVersionCommand extends EmulatorCommand {
+class QueryVersionCommand extends ExecCommand {
   _getArgs() {
     return ['-version'];
   }
 }
 
-class LaunchCommand extends EmulatorCommand {
+class LaunchCommand extends ExecCommand {
   constructor(emulatorName, options) {
     super();
     this._args = this._getEmulatorArgs(emulatorName, options);


Test patch:
diff --git a/detox/src/devices/drivers/android/AndroidDriver.test.js b/detox/src/devices/drivers/android/AndroidDriver.test.js
index 97b147dc5..da76b5f80 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.test.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.test.js
@@ -6,11 +6,33 @@ describe('Android driver', () => {
   const mockGetAbsoluteBinaryPathImpl = (x) => `absolutePathOf(${x})`;
   const mockAPKPathGetTestApkPathImpl = (x) => `testApkPathOf(${x})`;
 
+  class MockADBClass {
+    constructor() {
+      Object.assign(this, adb);
+    }
+  }
+
+  class MockAsyncEmitterClass {
+    constructor() {
+      this.emit = jest.fn();
+    }
+  }
+
+  class MockInstrumentationLogsParserClass {
+    constructor() {
+      this.parse = () => {};
+      this.containsStackTraceLog = () => true;
+      this.getStackTrace = () => MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK;
+    }
+  }
+  MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK = 'Stacktrace mock';
+
   let logger;
   let client;
   let getAbsoluteBinaryPath;
   let fs;
   let exec;
+  let adb;
   beforeEach(() => {
     jest.mock('fs', () => ({
       existsSync: jest.fn(),
@@ -22,12 +44,11 @@ describe('Android driver', () => {
       encodeBase64: (x) => `base64(${x})`,
     }));
 
-    jest.mock('./tools/ADB', () => mockADBClass);
     jest.mock('../../../utils/sleep', () => jest.fn().mockResolvedValue(''));
     jest.mock('../../../utils/retry', () => jest.fn().mockResolvedValue(''));
 
     jest.mock('./InstrumentationLogsParser', () => ({
-      InstrumentationLogsParser: mockInstrumentationLogsParserClass,
+      InstrumentationLogsParser: MockInstrumentationLogsParserClass,
     }));
 
     jest.mock('../../../utils/getAbsoluteBinaryPath', () =>
@@ -49,15 +70,6 @@ describe('Android driver', () => {
     logger = require('../../../utils/logger');
 
     jest.mock('../../../utils/exec', () => ({
-      spawnAndLog: jest.fn().mockReturnValue({
-        childProcess: {
-          on: jest.fn(),
-          stdout: {
-            setEncoding: jest.fn(),
-            on: jest.fn(),
-          }
-        }
-      }),
       interruptProcess: jest.fn(),
     }));
     exec = require('../../../utils/exec');
@@ -70,22 +82,40 @@ describe('Android driver', () => {
     };
   });
 
+  beforeEach(() => {
+    const ADB = jest.genMockFromModule('./tools/ADB');
+    adb = new ADB();
+    adb.adbBin = 'ADB binary mock';
+    adb.spawnInstrumentation = jest.fn().mockReturnValue({
+      childProcess: {
+        on: jest.fn(),
+        stdout: {
+          setEncoding: jest.fn(),
+          on: jest.fn(),
+        }
+      }
+    });
+    jest.mock('./tools/ADB', () => MockADBClass);
+  });
+
   let uut;
   beforeEach(() => {
     const AndroidDriver = require('./AndroidDriver');
     uut = new AndroidDriver({
       client,
-      emitter: new mockAsyncEmitter(),
+      emitter: new MockAsyncEmitterClass(),
     });
   });
 
   describe('Instrumentation bootstrap', () => {
     it('should launch instrumentation upon app launch', async () => {
+      adb.getInstrumentationRunner.mockResolvedValue('mock test-runner');
+
       await uut.launchApp(deviceId, bundleId, {}, '');
 
-      expect(exec.spawnAndLog).toHaveBeenCalledWith(
+      expect(adb.spawnInstrumentation).toHaveBeenCalledWith(
+        deviceId,
         expect.anything(),
-        expect.arrayContaining(['shell', 'am', 'instrument']),
         expect.anything(),
       );
     });
@@ -105,14 +135,14 @@ describe('Android driver', () => {
       const clientWaitResolve = mockDeviceReadyPromise();
 
       const promise = uut.waitUntilReady();
-      setTimeout(async () => await killInstrumentation(exec.spawnAndLog()), 1);
+      setTimeout(async () => await killInstrumentation(adb.spawnInstrumentation()), 1);
 
       try {
         await promise;
         fail('Expected an error and none was thrown');
       } catch (e) {
         expect(e.toString()).toContain('DetoxRuntimeError: Failed to run application on the device');
-        expect(e.toString()).toContain(`Native stacktrace dump: ${mockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK}`);
+        expect(e.toString()).toContain(`Native stacktrace dump: ${MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK}`);
       } finally {
         clientWaitResolve();
       }
@@ -161,10 +191,10 @@ describe('Android driver', () => {
 
       await uut.launchApp(deviceId, bundleId, launchArgs, '');
 
-      const spawnArgs = exec.spawnAndLog.mock.calls[0];
+      const spawnArgs = adb.spawnInstrumentation.mock.calls[0];
       const spawnedFlags = spawnArgs[1];
 
-      let index = 7;
+      let index = 0;
       index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({
         key: 'object-arg',
         value: 'base64({"such":"wow","much":"amaze","very":111})'
@@ -196,10 +226,10 @@ describe('Android driver', () => {
 
       await uut.launchApp(deviceId, bundleId, launchArgs, '');
 
-      const spawnArgs = exec.spawnAndLog.mock.calls[0];
+      const spawnArgs = adb.spawnInstrumentation.mock.calls[0];
       const spawnedFlags = spawnArgs[1];
 
-      let index = 10;
+      let index = 3;
       index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'class', value: 'class-value' });
       index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'package', value: 'package-value' });
       index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'func', value: 'func-value' });
@@ -291,30 +321,3 @@ describe('Android driver', () => {
     });
   });
 });
-
-class mockADBClass {
-  constructor() {
-    this.getInstrumentationRunner = jest.fn();
-    this.reverse = jest.fn();
-    this.reverseRemove = jest.fn();
-    this.install = jest.fn();
-
-    this.adbBin = 'ADB binary mock';
-  }
-}
-
-class mockAsyncEmitter {
-  constructor() {
-    this.emit = jest.fn();
-  }
-}
-
-class mockInstrumentationLogsParserClass {
-  constructor() {
-    this.parse = () => {};
-    this.containsStackTraceLog = () => true;
-    this.getStackTrace = () => mockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK;
-  }
-}
-
-mockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK = 'Stacktrace mock';
diff --git a/detox/src/devices/drivers/android/tools/ADB.test.js b/detox/src/devices/drivers/android/tools/ADB.test.js
index 3d70003f8..943c1129e 100644
--- a/detox/src/devices/drivers/android/tools/ADB.test.js
+++ b/detox/src/devices/drivers/android/tools/ADB.test.js
@@ -5,6 +5,7 @@ describe('ADB', () => {
   let ADB;
   let adb;
   let exec;
+  let spawn;
 
   beforeEach(() => {
     jest.mock('../../../../utils/logger');
@@ -29,12 +30,12 @@ describe('ADB', () => {
     }
     jest.mock('./EmulatorTelnet', () => MockEmulatorTelnet);
 
-    jest.mock('../../../../utils/exec', () => {
-      const exec = jest.fn();
-      exec.mockReturnValue({ stdout: '' });
-      return { execWithRetriesAndLogs: exec };
-    });
+    jest.mock('../../../../utils/exec', () => ({
+      execWithRetriesAndLogs: jest.fn().mockReturnValue({ stdout: '' }),
+      spawnAndLog: jest.fn(),
+    }));
     exec = require('../../../../utils/exec').execWithRetriesAndLogs;
+    spawn = require('../../../../utils/exec').spawnAndLog;
 
     ADB = require('./ADB');
     adb = new ADB();
@@ -243,6 +244,44 @@ describe('ADB', () => {
     });
   });
 
+  describe('spawnInstrumentation', () => {
+    const deviceId = 'aDeviceId';
+    const testRunner = 'aTestRunner';
+
+    it('should spawn instrumentation', async () => {
+      const userArgs = [];
+      const expectedArgs = ['-s', deviceId, 'shell', 'am', 'instrument', '-w', '-r', testRunner];
+      await adb.spawnInstrumentation(deviceId, userArgs, testRunner);
+      expect(spawn).toHaveBeenCalledWith(adbBinPath, expectedArgs, expect.any(Object));
+    });
+
+    it('should pass through additional args', async () => {
+      const userArgs = ['-mock', '-args'];
+      await adb.spawnInstrumentation(deviceId, userArgs, testRunner);
+      expect(spawn).toHaveBeenCalledWith(adbBinPath, expect.arrayContaining([...userArgs, testRunner]), expect.any(Object));
+    });
+
+    it('should set detaching=false as spawn-options', async () => {
+      const expectedOptions = {
+        detached: false,
+      };
+      const userArgs = [];
+
+      await adb.spawnInstrumentation(deviceId, userArgs, testRunner);
+
+      expect(spawn).toHaveBeenCalledWith(adbBinPath, expect.any(Array), expectedOptions);
+    });
+
+    it('should chain-return the promise from spawn util', async () => {
+      const userArgs = [];
+      const mockPromise = Promise.resolve('mock');
+      spawn.mockReturnValue(mockPromise);
+
+      const childProcessPromise = adb.spawnInstrumentation(deviceId, userArgs, testRunner);
+      expect(childProcessPromise).toEqual(mockPromise);
+    });
+  });
+
   it(`listInstrumentation passes the right deviceId`, async () => {
     const deviceId = 'aDeviceId';
     jest.spyOn(adb, 'shell');
diff --git a/detox/src/devices/drivers/android/tools/BinaryExec.test.js b/detox/src/devices/drivers/android/tools/BinaryExec.test.js
new file mode 100644
index 000000000..e1e16fcae
--- /dev/null
+++ b/detox/src/devices/drivers/android/tools/BinaryExec.test.js
@@ -0,0 +1,119 @@
+describe('BinaryExec', () => {
+  const binaryPath = "/binary/mock";
+
+  let exec;
+  let spawn;
+  let binaryExec;
+  beforeEach(() => {
+    jest.mock('../../../../utils/exec', () => ({
+      execWithRetriesAndLogs: jest.fn().mockResolvedValue({
+        stdout: '',
+      }),
+    }));
+    exec = require('../../../../utils/exec').execWithRetriesAndLogs;
+
+    jest.mock('child-process-promise', () => ({
+      spawn: jest.fn()
+    }));
+    spawn = require('child-process-promise').spawn;
+
+    const {BinaryExec} = require('./BinaryExec');
+    binaryExec = new BinaryExec(binaryPath);
+  });
+
+  it('should return the binary-path as the toString()', async () => {
+    expect(binaryExec.toString()).toEqual(binaryPath);
+  });
+
+  describe('exec-command', () => {
+    it('exec-command should return args as toString()', () => {
+      const {ExecCommand} = require('./BinaryExec');
+      class MockExecCommand extends ExecCommand {
+        _getArgsString() {
+          return 'test args';
+        }
+      }
+
+      expect(new MockExecCommand().toString()).toEqual('test args');
+    });
+  });
+
+  describe('exec', () => {
+    it('should execute the binary with a real command', async () => {
+      const command = anEmptyCommand();
+
+      await binaryExec.exec(command);
+
+      expect(exec).toHaveBeenCalledWith(`"${binaryPath}" `);
+    });
+
+    it('should utilize command-args', async () => {
+      const command = aCommandMockWithArgsString('-mock -argz');
+
+      await binaryExec.exec(command);
+
+      expect(exec).toHaveBeenCalledWith(`"${binaryPath}" -mock -argz`);
+    });
+
+    it('should return content of resolved promise\'s stdout', async () => {
+      const execResult = {
+        stdout: 'mock stdout content'
+      };
+      exec.mockResolvedValue(execResult);
+
+      const {ExecCommand} = require('./BinaryExec');
+      const command = new ExecCommand();
+      const result = await binaryExec.exec(command);
+
+      expect(result).toEqual(execResult.stdout);
+    });
+  });
+
+  describe('spawn', () => {
+    it('should spawn the binary with a real command', async () => {
+      const command = anEmptyCommand();
+
+      await binaryExec.spawn(command);
+
+      expect(spawn).toHaveBeenCalledWith(binaryPath, expect.anything(), expect.anything());
+    });
+
+    it('should pass through command-args', async () => {
+      const commandArgs = ['-mock', '-args'];
+      const command = aCommandMockWithArgs(commandArgs);
+
+      await binaryExec.spawn(command);
+
+      expect(spawn).toHaveBeenCalledWith(binaryPath, commandArgs, expect.anything());
+    });
+
+    it('should chain-return child-process-promise from spawn', async () => {
+      const childProcessPromise = Promise.resolve('mock result');
+      spawn.mockReturnValue(childProcessPromise);
+
+      const command = anEmptyCommand();
+
+      const result = binaryExec.spawn(command);
+      expect(result).toEqual(childProcessPromise);
+    });
+  });
+});
+
+const anEmptyCommand = () => {
+  const {ExecCommand} = require('./BinaryExec');
+  return new ExecCommand();
+};
+
+const aCommandMockWithArgsString = (argsString) => {
+  const {ExecCommand} = jest.genMockFromModule('./BinaryExec');
+  const command = new ExecCommand();
+  command._getArgsString.mockReturnValue(argsString);
+  return command;
+};
+
+const aCommandMockWithArgs = (commandArgs) => {
+  const {ExecCommand} = jest.genMockFromModule('./BinaryExec');
+  const command = new ExecCommand();
+  command._getArgs.mockReturnValue(commandArgs);
+  return command;
+};