-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Use case
The xcode_backend.dart mainly contains a Context class, and the current design is:
// xcode_backend.dart
class Context {
void func1() { ... }
void func2() { ... }
}
// xcode_backend_test.dart (unit tests in general.shard)
class TestContext {
@override
void func1() { ... }
@override
void func2() { ... }
}
For example, one of such functions is runSync:
flutter/packages/flutter_tools/bin/xcode_backend.dart
Lines 105 to 145 in 5483fef
| /// Run given command in a synchronous subprocess. | |
| /// | |
| /// Will throw [Exception] if the exit code is not 0. | |
| ProcessResult runSync( | |
| String bin, | |
| List<String> args, { | |
| bool verbose = false, | |
| bool allowFail = false, | |
| String? workingDirectory, | |
| }) { | |
| if (verbose) { | |
| print('♦ $bin ${args.join(' ')}'); | |
| } | |
| final ProcessResult result = Process.runSync(bin, args, workingDirectory: workingDirectory); | |
| if (verbose) { | |
| print((result.stdout as String).trim()); | |
| } | |
| final String resultStderr = result.stderr.toString().trim(); | |
| if (resultStderr.isNotEmpty) { | |
| final errorOutput = StringBuffer(); | |
| if (result.exitCode != 0) { | |
| // "error:" prefix makes this show up as an Xcode compilation error. | |
| errorOutput.write('error: '); | |
| } | |
| errorOutput.write(resultStderr); | |
| echoError(errorOutput.toString()); | |
| // Stream stderr to the Flutter build process. | |
| // When in verbose mode, `echoError` above will show the logs. So only | |
| // stream if not in verbose mode to avoid duplicate logs. | |
| // Also, only stream if exitCode is 0 since errors are handled separately | |
| // by the tool on failure. | |
| if (!verbose && exitCode == 0) { | |
| streamOutput(errorOutput.toString()); | |
| } | |
| } | |
| if (!allowFail && result.exitCode != 0) { | |
| throw Exception('Command "$bin ${args.join(' ')}" exited with code ${result.exitCode}'); | |
| } | |
| return result; | |
| } |
And it's overwritten here:
flutter/packages/flutter_tools/test/general.shard/xcode_backend_test.dart
Lines 1056 to 1069 in 5483fef
| @override | |
| ProcessResult runSync( | |
| String bin, | |
| List<String> args, { | |
| bool verbose = false, | |
| bool allowFail = false, | |
| String? workingDirectory, | |
| }) { | |
| return processManager.runSync( | |
| <dynamic>[bin, ...args], | |
| workingDirectory: workingDirectory, | |
| environment: environment, | |
| ); | |
| } |
This means during unit test, the whole chunk of actual implementation is stubbed out, which defeats the whole purpose of unit tests! For unit test, we should stub out the dependencies of the test subject, not the implementation of test subject itself.
Proposal
Quick fix (short term)
Context can instead introduce a 1-line wrapper function that only contains https://github.com/flutter/flutter/blob/5483fef5384821748cfcc1bfc3157e1f9c0322ea/packages/flutter_tools/bin/xcode_backend.dart#L118C34-L118C41
Then TestContext will just override this wrapper function. Then the rest of the implementation would be run during unit test.
Actual fix
The fact that Context is only inherited by TestContext in tests is an indicator that inheritance isn't the best approach here. We should instead favor composition, by injecting dependencies (e.g. ProcessManager).