Replace MockXcode with Xcode.test in unit tests (#74777)

diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 3ccdc34..9ea4427 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:file/memory.dart';
 import 'package:meta/meta.dart';
 import 'package:process/process.dart';
 
@@ -229,25 +230,78 @@
 
 /// Interpreter of Xcode projects.
 class XcodeProjectInterpreter {
-  XcodeProjectInterpreter({
+  factory XcodeProjectInterpreter({
     @required Platform platform,
     @required ProcessManager processManager,
     @required Logger logger,
     @required FileSystem fileSystem,
     @required Terminal terminal,
     @required Usage usage,
+  }) {
+    return XcodeProjectInterpreter._(
+      platform: platform,
+      processManager: processManager,
+      logger: logger,
+      fileSystem: fileSystem,
+      terminal: terminal,
+      usage: usage,
+    );
+  }
+
+  XcodeProjectInterpreter._({
+    @required Platform platform,
+    @required ProcessManager processManager,
+    @required Logger logger,
+    @required FileSystem fileSystem,
+    @required Terminal terminal,
+    @required Usage usage,
+    int majorVersion,
+    int minorVersion,
+    int patchVersion,
   }) : _platform = platform,
-      _fileSystem = fileSystem,
-      _terminal = terminal,
-      _logger = logger,
-      _processUtils = ProcessUtils(logger: logger, processManager: processManager),
-      _operatingSystemUtils = OperatingSystemUtils(
-        fileSystem: fileSystem,
-        logger: logger,
-        platform: platform,
-        processManager: processManager,
-      ),
-      _usage = usage;
+        _fileSystem = fileSystem,
+        _terminal = terminal,
+        _logger = logger,
+        _processUtils = ProcessUtils(logger: logger, processManager: processManager),
+        _operatingSystemUtils = OperatingSystemUtils(
+          fileSystem: fileSystem,
+          logger: logger,
+          platform: platform,
+          processManager: processManager,
+        ),
+        _majorVersion = majorVersion,
+        _minorVersion = minorVersion,
+        _patchVersion = patchVersion,
+        _usage = usage;
+
+  /// Create an [XcodeProjectInterpreter] for testing.
+  ///
+  /// Defaults to installed with sufficient version,
+  /// a memory file system, fake platform, buffer logger,
+  /// test [Usage], and test [Terminal].
+  /// Set [majorVersion] to null to simulate Xcode not being installed.
+  factory XcodeProjectInterpreter.test({
+    @required ProcessManager processManager,
+    int majorVersion = 1000,
+    int minorVersion = 0,
+    int patchVersion = 0,
+  }) {
+    final Platform platform = FakePlatform(
+      operatingSystem: 'macos',
+      environment: <String, String>{},
+    );
+    return XcodeProjectInterpreter._(
+      fileSystem: MemoryFileSystem.test(),
+      platform: platform,
+      processManager: processManager,
+      usage: Usage.test(),
+      logger: BufferLogger.test(),
+      terminal: Terminal.test(),
+      majorVersion: majorVersion,
+      minorVersion: minorVersion,
+      patchVersion: patchVersion,
+    );
+  }
 
   final Platform _platform;
   final FileSystem _fileSystem;
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart
index 2691098..99a4008 100644
--- a/packages/flutter_tools/lib/src/macos/xcode.dart
+++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:file/memory.dart';
 import 'package:meta/meta.dart';
 import 'package:process/process.dart';
 
@@ -55,6 +56,30 @@
         _processUtils =
             ProcessUtils(logger: logger, processManager: processManager);
 
+  /// Create an [Xcode] for testing.
+  ///
+  /// Defaults to a memory file system, fake platform,
+  /// buffer logger, and test [XcodeProjectInterpreter].
+  @visibleForTesting
+  factory Xcode.test({
+    @required ProcessManager processManager,
+    XcodeProjectInterpreter xcodeProjectInterpreter,
+    Platform platform,
+    FileSystem fileSystem,
+  }) {
+    platform ??= FakePlatform(
+      operatingSystem: 'macos',
+      environment: <String, String>{},
+    );
+    return Xcode(
+      platform: platform,
+      processManager: processManager,
+      fileSystem: fileSystem ?? MemoryFileSystem.test(),
+      logger: BufferLogger.test(),
+      xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
+    );
+  }
+
   final Platform _platform;
   final ProcessUtils _processUtils;
   final FileSystem _fileSystem;
@@ -78,12 +103,7 @@
     return _xcodeSelectPath;
   }
 
-  bool get isInstalled {
-    if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) {
-      return false;
-    }
-    return _xcodeProjectInterpreter.isInstalled;
-  }
+  bool get isInstalled => _xcodeProjectInterpreter.isInstalled;
 
   Version get currentVersion => Version(
         _xcodeProjectInterpreter.majorVersion,
diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
index 68f4af6..693ca62 100644
--- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
@@ -4,7 +4,6 @@
 
 import 'dart:async';
 
-import 'package:file/memory.dart';
 import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
 import 'package:flutter_tools/src/base/logger.dart';
@@ -43,10 +42,7 @@
 
       setUp(() {
         mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
-        xcode = Xcode(
-          logger: logger,
-          platform: FakePlatform(operatingSystem: 'macos'),
-          fileSystem: MemoryFileSystem.test(),
+        xcode = Xcode.test(
           processManager: processManager,
           xcodeProjectInterpreter: mockXcodeProjectInterpreter,
         );
@@ -78,25 +74,27 @@
 
     group('xcdevice', () {
       XCDevice xcdevice;
-      MockXcode mockXcode;
+      Xcode xcode;
 
       setUp(() {
-        mockXcode = MockXcode();
+        xcode = Xcode.test(
+          processManager: FakeProcessManager.any(),
+          xcodeProjectInterpreter: XcodeProjectInterpreter.test(
+            processManager: FakeProcessManager.any(),
+          ),
+        );
         xcdevice = XCDevice(
           processManager: processManager,
           logger: logger,
-          xcode: mockXcode,
+          xcode: xcode,
           platform: null,
           artifacts: Artifacts.test(),
           cache: Cache.test(),
           iproxy: IProxy.test(logger: logger, processManager: processManager),
         );
-        when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
       });
 
       testWithoutContext('available devices xcdevice fails', () async {
-        when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
         when(processManager.run(<String>['xcrun', 'xcdevice', 'list', '--timeout', '2']))
           .thenThrow(const ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2']));
 
@@ -104,8 +102,6 @@
       });
 
       testWithoutContext('diagnostics xcdevice fails', () async {
-        when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
         when(processManager.run(<String>['xcrun', 'xcdevice', 'list', '--timeout', '2']))
           .thenThrow(const ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2']));
 
@@ -129,10 +125,8 @@
       });
 
       testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () {
-        final Xcode xcode = Xcode(
-          logger: logger,
+        final Xcode xcode = Xcode.test(
           platform: FakePlatform(operatingSystem: 'windows'),
-          fileSystem: MemoryFileSystem.test(),
           processManager: fakeProcessManager,
           xcodeProjectInterpreter: mockXcodeProjectInterpreter,
         );
@@ -151,10 +145,7 @@
             ],
           ),
         );
-        final Xcode xcode = Xcode(
-          logger: logger,
-          platform: FakePlatform(operatingSystem: 'macos'),
-          fileSystem: MemoryFileSystem.test(),
+        final Xcode xcode = Xcode.test(
           processManager: fakeProcessManager,
           xcodeProjectInterpreter: mockXcodeProjectInterpreter,
         );
@@ -175,10 +166,7 @@
             exitCode: 1,
           ),
         );
-        final Xcode xcode = Xcode(
-          logger: logger,
-          platform: FakePlatform(operatingSystem: 'macos'),
-          fileSystem: MemoryFileSystem.test(),
+        final Xcode xcode = Xcode.test(
           processManager: fakeProcessManager,
           xcodeProjectInterpreter: mockXcodeProjectInterpreter,
         );
@@ -189,16 +177,11 @@
 
       group('macOS', () {
         Xcode xcode;
-        FakePlatform platform;
 
         setUp(() {
           mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
           when(mockXcodeProjectInterpreter.xcrunCommand()).thenReturn(<String>['xcrun']);
-          platform = FakePlatform(operatingSystem: 'macos');
-          xcode = Xcode(
-            logger: logger,
-            platform: platform,
-            fileSystem: MemoryFileSystem.test(),
+          xcode = Xcode.test(
             processManager: fakeProcessManager,
             xcodeProjectInterpreter: mockXcodeProjectInterpreter,
           );
@@ -318,36 +301,13 @@
         });
 
         testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () {
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['/usr/bin/xcode-select', '--print-path'],
-            stdout: '/Applications/Xcode8.0.app/Contents/Developer',
-          ));
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
 
           expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
           expect(fakeProcessManager.hasRemainingExpectations, isFalse);
         });
 
-        testWithoutContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () {
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['/usr/bin/xcode-select', '--print-path'],
-            exitCode: 127,
-            stderr: 'ERROR',
-          ));
-          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-          expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
-          expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-        });
-
         testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () {
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['/usr/bin/xcode-select', '--print-path'],
-            stdout: '/Applications/Xcode8.0.app/Contents/Developer',
-          ));
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
           when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
           when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
@@ -358,10 +318,6 @@
         });
 
         testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () {
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['/usr/bin/xcode-select', '--print-path'],
-            stdout: '/Applications/Xcode8.0.app/Contents/Developer',
-          ));
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
           when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
           when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
@@ -432,42 +388,59 @@
       });
     });
 
-    group('xcdevice', () {
+    group('xcdevice not installed', () {
       XCDevice xcdevice;
-      MockXcode mockXcode;
+      Xcode xcode;
 
       setUp(() {
-        mockXcode = MockXcode();
+        xcode = Xcode.test(
+          processManager: FakeProcessManager.any(),
+          xcodeProjectInterpreter: XcodeProjectInterpreter.test(
+            processManager: FakeProcessManager.any(),
+            majorVersion: null, // Not installed.
+          ),
+        );
         xcdevice = XCDevice(
           processManager: fakeProcessManager,
           logger: logger,
-          xcode: mockXcode,
+          xcode: xcode,
           platform: null,
           artifacts: Artifacts.test(),
           cache: Cache.test(),
           iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
         );
-        when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
       });
 
-      group('installed', () {
-        testWithoutContext('Xcode not installed', () {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
-          expect(xcdevice.isInstalled, false);
-        });
+      testWithoutContext('Xcode not installed', () async {
+        expect(xcode.isInstalled, false);
+
+        expect(xcdevice.isInstalled, false);
+        expect(xcdevice.observedDeviceEvents(), isNull);
+        expect(logger.traceText, contains("Xcode not found. Run 'flutter doctor' for more information."));
+        expect(await xcdevice.getAvailableIOSDevices(), isEmpty);
+        expect(await xcdevice.getDiagnostics(), isEmpty);
+      });
+    });
+
+    group('xcdevice', () {
+      XCDevice xcdevice;
+      Xcode xcode;
+
+      setUp(() {
+        xcode = Xcode.test(processManager: FakeProcessManager.any());
+        xcdevice = XCDevice(
+          processManager: fakeProcessManager,
+          logger: logger,
+          xcode: xcode,
+          platform: null,
+          artifacts: Artifacts.test(),
+          cache: Cache.test(),
+          iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
+        );
       });
 
       group('observe device events', () {
-        testWithoutContext('Xcode not installed', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
-
-          expect(xcdevice.observedDeviceEvents(), isNull);
-          expect(logger.traceText, contains("Xcode not found. Run 'flutter doctor' for more information."));
-        });
-
         testUsingContext('relays events', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
           fakeProcessManager.addCommand(const FakeCommand(
             command: <String>[
               'script',
@@ -516,16 +489,7 @@
 
       group('available devices', () {
         final FakePlatform macPlatform = FakePlatform(operatingSystem: 'macos');
-
-        testWithoutContext('Xcode not installed', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
-
-          expect(await xcdevice.getAvailableIOSDevices(), isEmpty);
-        });
-
         testUsingContext('returns devices', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
           const String devicesOutput = '''
 [
   {
@@ -644,8 +608,6 @@
         });
 
         testWithoutContext('uses timeout', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
           fakeProcessManager.addCommand(const FakeCommand(
             command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '20'],
             stdout: '[]',
@@ -655,8 +617,6 @@
         });
 
         testUsingContext('ignores "Preparing debugger support for iPhone" error', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
           const String devicesOutput = '''
 [
   {
@@ -695,8 +655,6 @@
         });
 
         testUsingContext('handles unknown architectures', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
           const String devicesOutput = '''
 [
   {
@@ -742,16 +700,7 @@
 
       group('diagnostics', () {
         final FakePlatform macPlatform = FakePlatform(operatingSystem: 'macos');
-
-        testWithoutContext('Xcode not installed', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
-
-          expect(await xcdevice.getDiagnostics(), isEmpty);
-        });
-
         testUsingContext('uses cache', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
           const String devicesOutput = '''
 [
   {
@@ -787,8 +736,6 @@
         });
 
         testUsingContext('returns error message', () async {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
           const String devicesOutput = '''
 [
    {
@@ -894,6 +841,5 @@
   });
 }
 
-class MockXcode extends Mock implements Xcode {}
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}