Make MockBuilder support build_extensions option.
This is useful to change the destination of the generated files.
i.e: instead of having them on the same folder,
you can specify a diferent folder for the mocks.
Closes https://github.com/dart-lang/mockito/issues/545
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7da3cb8..4d1dba8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
* Require analyzer 5.12.0, allow analyzer version 6.x;
* Add example of writing a class to mock function objects.
+* Add support for the `build_extensions` build.yaml option
## 5.4.2
diff --git a/FAQ.md b/FAQ.md
index 53ca021..f203360 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -167,3 +167,37 @@
[`verify`]: https://pub.dev/documentation/mockito/latest/mockito/verify.html
[`verifyInOrder`]: https://pub.dev/documentation/mockito/latest/mockito/verifyInOrder.html
+
+
+### How can I customize where Mockito outputs its mocks?
+
+Mockito supports configuration of outputs by the configuration provided by the `build`
+package by creating (if it doesn't exist already) the `build.yaml` at the root folder
+of the project.
+
+It uses the `build_extensions` option, which can be used to alter not only the output directory but you
+can also do other filename manipulation, eg.: append/prepend strings to the filename or add another extension
+to the filename.
+
+To use `build_extensions` you can use `^` on the input string to match on the project root, and `{{}}` to capture the remaining path/filename.
+
+You can also have multiple build_extensions options, but they can't conflict with each other.
+For consistency, the output pattern must always end with `.mocks.dart` and the input pattern must always end with `.dart`
+
+```yaml
+targets:
+ $default:
+ builders:
+ mockito|mockBuilder:
+ generate_for:
+ options:
+ # build_extensions takes a source pattern and if it matches it will transform the output
+ # to your desired path. The default behaviour is to the .mocks.dart file to be in the same
+ # directory as the source .dart file. As seen below this is customizable, but the generated
+ # file must always end in `.mocks.dart`.
+ build_extensions:
+ '^tests/{{}}.dart' : 'tests/mocks/{{}}.mocks.dart'
+ '^integration-tests/{{}}.dart' : 'integration-tests/{{}}.mocks.dart'
+```
+
+Also, you can also check out the example configuration in the Mockito repository.
diff --git a/build.yaml b/build.yaml
index 0b4fb40..1c44479 100644
--- a/build.yaml
+++ b/build.yaml
@@ -5,6 +5,13 @@
generate_for:
- example/**.dart
- test/end2end/*.dart
+ options:
+ # build_extensions takes a source pattern and if it matches it will transform the output
+ # to your desired path. The default behaviour is to the .mocks.dart file to be in the same
+ # directory as the source .dart file. As seen below this is customizable, but the generated
+ # file must always end in `.mocks.dart`.
+ build_extensions:
+ '^example/build_extensions/{{}}.dart' : 'example/build_extensions/mocks/{{}}.mocks.dart'
builders:
mockBuilder:
diff --git a/example/build_extensions/example.dart b/example/build_extensions/example.dart
new file mode 100644
index 0000000..fde5ca4
--- /dev/null
+++ b/example/build_extensions/example.dart
@@ -0,0 +1,40 @@
+// Copyright 2023 Dart Mockito authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test_api/scaffolding.dart';
+
+// Because we customized the `build_extensions` option, we can output
+// the generated mocks in a diferent directory
+import 'mocks/example.mocks.dart';
+
+class Dog {
+ String sound() => "bark";
+ bool? eatFood(String? food) => true;
+ Future<void> chew() async => print('Chewing...');
+ int? walk(List<String>? places) => 1;
+}
+
+@GenerateNiceMocks([MockSpec<Dog>()])
+void main() {
+ test("Verify some dog behaviour", () async {
+ MockDog mockDog = MockDog();
+ when(mockDog.eatFood(any));
+
+ mockDog.eatFood("biscuits");
+
+ verify(mockDog.eatFood(any)).called(1);
+ });
+}
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index 19381dd..a9d53f7 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -63,18 +63,40 @@
/// 'foo.mocks.dart' will be created.
class MockBuilder implements Builder {
@override
+ final Map<String, List<String>> buildExtensions = {
+ '.dart': ['.mocks.dart']
+ };
+
+ MockBuilder({Map<String, List<String>>? buildExtensions}) {
+ this.buildExtensions.addAll(buildExtensions ?? {});
+ }
+
+ @override
Future<void> build(BuildStep buildStep) async {
if (!await buildStep.resolver.isLibrary(buildStep.inputId)) return;
final entryLib = await buildStep.inputLibrary;
final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
- final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
+
+ // While it can be acceptable that we get more than 2 allowedOutputs,
+ // because it's the general one and the user defined one. Having
+ // more, means that user has conflicting patterns so we should throw.
+ if (buildStep.allowedOutputs.length > 2) {
+ throw ArgumentError('Build_extensions has conflicting outputs on file '
+ '`${buildStep.inputId.path}`, it usually caused by missconfiguration '
+ 'on your `build.yaml` file');
+ }
+ // if not single, we always choose the user defined one.
+ final mockLibraryAsset = buildStep.allowedOutputs.singleOrNull ??
+ buildStep.allowedOutputs
+ .where((element) =>
+ element != buildStep.inputId.changeExtension('.mocks.dart'))
+ .single;
final inheritanceManager = InheritanceManager3();
final mockTargetGatherer =
_MockTargetGatherer(entryLib, inheritanceManager);
- final entryAssetId = await buildStep.resolver.assetIdForElement(entryLib);
final assetUris = await _resolveAssetUris(buildStep.resolver,
- mockTargetGatherer._mockTargets, entryAssetId.path, entryLib);
+ mockTargetGatherer._mockTargets, mockLibraryAsset.path, entryLib);
final mockLibraryInfo = _MockLibraryInfo(mockTargetGatherer._mockTargets,
assetUris: assetUris,
@@ -240,11 +262,6 @@
}
return element.library!;
}
-
- @override
- final buildExtensions = const {
- '.dart': ['.mocks.dart']
- };
}
/// An [Element] visitor which collects the elements of all of the
@@ -2304,7 +2321,29 @@
}
/// A [MockBuilder] instance for use by `build.yaml`.
-Builder buildMocks(BuilderOptions options) => MockBuilder();
+Builder buildMocks(BuilderOptions options) {
+ final buildExtensions = options.config['build_extensions'];
+ if (buildExtensions == null) return MockBuilder();
+ if (buildExtensions is! Map) {
+ throw ArgumentError(
+ 'build_extensions should be a map from inputs to outputs');
+ }
+ final result = <String, List<String>>{};
+ for (final entry in buildExtensions.entries) {
+ final input = entry.key;
+ final output = entry.value;
+ if (input is! String || !input.endsWith('.dart')) {
+ throw ArgumentError('Invalid key in build_extensions `$input`, it '
+ 'should be a string ending with `.dart`');
+ }
+ if (output is! String || !output.endsWith('.mocks.dart')) {
+ throw ArgumentError('Invalid key in build_extensions `$output`, it '
+ 'should be a string ending with `mocks.dart`');
+ }
+ result[input] = [output];
+ }
+ return MockBuilder(buildExtensions: result);
+}
extension on Element {
/// Returns the "full name" of a class or method element.
diff --git a/test/builder/auto_mocks_test.dart b/test/builder/auto_mocks_test.dart
index 1a7b0e0..f3237cc 100644
--- a/test/builder/auto_mocks_test.dart
+++ b/test/builder/auto_mocks_test.dart
@@ -22,8 +22,6 @@
import 'package:package_config/package_config.dart';
import 'package:test/test.dart';
-Builder buildMocks(BuilderOptions options) => MockBuilder();
-
const annotationsAsset = {
'mockito|lib/annotations.dart': '''
class GenerateMocks {
@@ -86,25 +84,27 @@
/// Test [MockBuilder] in a package which has not opted into null safety.
Future<void> testPreNonNullable(Map<String, String> sourceAssets,
- {Map<String, /*String|Matcher<String>*/ Object>? outputs}) async {
+ {Map<String, /*String|Matcher<String>*/ Object>? outputs,
+ Map<String, dynamic> config = const <String, dynamic>{}}) async {
final packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(2, 7))
]);
- await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+ await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
writer: writer, outputs: outputs, packageConfig: packageConfig);
}
/// Test [MockBuilder] in a package which has opted into null safety.
Future<void> testWithNonNullable(Map<String, String> sourceAssets,
- {Map<String, /*String|Matcher<List<int>>*/ Object>? outputs}) async {
+ {Map<String, /*String|Matcher<List<int>>*/ Object>? outputs,
+ Map<String, dynamic> config = const <String, dynamic>{}}) async {
final packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(3, 0))
]);
- await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+ await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
writer: writer, outputs: outputs, packageConfig: packageConfig);
}
@@ -3662,6 +3662,114 @@
contains('bar: _FakeBar_0('))));
});
});
+
+ group('build_extensions support', () {
+ test('should export mocks to different directory', () async {
+ await testWithNonNullable({
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': '''
+ import 'bar.dart';
+ class Foo extends Bar {}
+ ''',
+ 'foo|lib/bar.dart': '''
+ import 'dart:async';
+ class Bar {
+ m(Future<void> a) {}
+ }
+ ''',
+ }, config: {
+ "build_extensions": {"^test/{{}}.dart": "test/mocks/{{}}.mocks.dart"}
+ });
+ final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+ final mocksContent = utf8.decode(writer.assets[mocksAsset]!);
+ expect(mocksContent, contains("import 'dart:async' as _i3;"));
+ expect(mocksContent, contains('m(_i3.Future<void>? a)'));
+ });
+
+ test('should throw if it has confilicting outputs', () async {
+ await expectLater(
+ testWithNonNullable({
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': '''
+ import 'bar.dart';
+ class Foo extends Bar {}
+ ''',
+ 'foo|lib/bar.dart': '''
+ import 'dart:async';
+ class Bar {
+ m(Future<void> a) {}
+ }
+ ''',
+ }, config: {
+ "build_extensions": {
+ "^test/{{}}.dart": "test/mocks/{{}}.mocks.dart",
+ "test/{{}}.dart": "test/{{}}.something.mocks.dart"
+ }
+ }),
+ throwsArgumentError);
+ final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+ final otherMocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+ final somethingMocksAsset =
+ AssetId('foo', 'test/mocks/foo_test.something.mocks.dart');
+
+ expect(writer.assets.containsKey(mocksAsset), false);
+ expect(writer.assets.containsKey(otherMocksAsset), false);
+ expect(writer.assets.containsKey(somethingMocksAsset), false);
+ });
+
+ test('should throw if input is in incorrect format', () async {
+ await expectLater(
+ testWithNonNullable({
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': '''
+ import 'bar.dart';
+ class Foo extends Bar {}
+ ''',
+ 'foo|lib/bar.dart': '''
+ import 'dart:async';
+ class Bar {
+ m(Future<void> a) {}
+ }
+ ''',
+ }, config: {
+ "build_extensions": {"^test/{{}}": "test/mocks/{{}}.mocks.dart"}
+ }),
+ throwsArgumentError);
+ final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+ final mocksAssetOriginal = AssetId('foo', 'test/foo_test.mocks.dart');
+
+ expect(writer.assets.containsKey(mocksAsset), false);
+ expect(writer.assets.containsKey(mocksAssetOriginal), false);
+ });
+
+ test('should throw if output is in incorrect format', () async {
+ await expectLater(
+ testWithNonNullable({
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': '''
+ import 'bar.dart';
+ class Foo extends Bar {}
+ ''',
+ 'foo|lib/bar.dart': '''
+ import 'dart:async';
+ class Bar {
+ m(Future<void> a) {}
+ }
+ ''',
+ }, config: {
+ "build_extensions": {"^test/{{}}.dart": "test/mocks/{{}}.g.dart"}
+ }),
+ throwsArgumentError);
+ final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+ final mocksAssetOriginal = AssetId('foo', 'test/foo_test.mocks.dart');
+ expect(writer.assets.containsKey(mocksAsset), false);
+ expect(writer.assets.containsKey(mocksAssetOriginal), false);
+ });
+ });
}
TypeMatcher<List<int>> _containsAllOf(a, [b]) => decodedMatches(