Skip to content

Commit 4ff995f

Browse files
committed
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 #545
1 parent ff79de6 commit 4ff995f

File tree

6 files changed

+244
-15
lines changed

6 files changed

+244
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* Require analyzer 5.12.0, allow analyzer version 6.x;
44
* Add example of writing a class to mock function objects.
5+
* Add support for the `build_extensions` build.yaml option
56

67
## 5.4.2
78

FAQ.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,37 @@ it's done. It's very straightforward.
167167

168168
[`verify`]: https://pub.dev/documentation/mockito/latest/mockito/verify.html
169169
[`verifyInOrder`]: https://pub.dev/documentation/mockito/latest/mockito/verifyInOrder.html
170+
171+
172+
### How can I customize where Mockito outputs its mocks?
173+
174+
Mockito supports configuration of outputs by the configuration provided by the `build`
175+
package by creating (if it doesn't exist already) the `build.yaml` at the root folder
176+
of the project.
177+
178+
It uses the `build_extensions` option, which can be used to alter not only the output directory but you
179+
can also do other filename manipulation, eg.: append/prepend strings to the filename or add another extension
180+
to the filename.
181+
182+
To use `build_extensions` you can use `^` on the input string to match on the project root, and `{{}}` to capture the remaining path/filename.
183+
184+
You can also have multiple build_extensions options, but they can't conflict with each other.
185+
For consistency, the output pattern must always end with `.mocks.dart` and the input pattern must always end with `.dart`
186+
187+
```yaml
188+
targets:
189+
$default:
190+
builders:
191+
mockito|mockBuilder:
192+
generate_for:
193+
options:
194+
# build_extensions takes a source pattern and if it matches it will transform the output
195+
# to your desired path. The default behaviour is to the .mocks.dart file to be in the same
196+
# directory as the source .dart file. As seen below this is customizable, but the generated
197+
# file must always end in `.mocks.dart`.
198+
build_extensions:
199+
'^tests/{{}}.dart' : 'tests/mocks/{{}}.mocks.dart'
200+
'^integration-tests/{{}}.dart' : 'integration-tests/{{}}.mocks.dart'
201+
```
202+
203+
Also, you can also check out the example configuration in the Mockito repository.

build.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ targets:
55
generate_for:
66
- example/**.dart
77
- test/end2end/*.dart
8+
options:
9+
# build_extensions takes a source pattern and if it matches it will transform the output
10+
# to your desired path. The default behaviour is to the .mocks.dart file to be in the same
11+
# directory as the source .dart file. As seen below this is customizable, but the generated
12+
# file must always end in `.mocks.dart`.
13+
build_extensions:
14+
'^example/build_extensions/{{}}.dart' : 'example/build_extensions/mocks/{{}}.mocks.dart'
815

916
builders:
1017
mockBuilder:

example/build_extensions/example.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2023 Dart Mockito authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:mockito/annotations.dart';
16+
import 'package:mockito/mockito.dart';
17+
import 'package:test_api/scaffolding.dart';
18+
19+
// Because we customized the `build_extensions` option, we can output
20+
// the generated mocks in a diferent directory
21+
import 'mocks/example.mocks.dart';
22+
23+
class Dog {
24+
String sound() => "bark";
25+
bool? eatFood(String? food) => true;
26+
Future<void> chew() async => print('Chewing...');
27+
int? walk(List<String>? places) => 1;
28+
}
29+
30+
@GenerateNiceMocks([MockSpec<Dog>()])
31+
void main() {
32+
test("Verify some dog behaviour", () async {
33+
MockDog mockDog = MockDog();
34+
when(mockDog.eatFood(any));
35+
36+
mockDog.eatFood("biscuits");
37+
38+
verify(mockDog.eatFood(any)).called(1);
39+
});
40+
}

lib/src/builder.dart

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,41 @@ import 'package:source_gen/source_gen.dart';
6262
/// will produce a "'.mocks.dart' file with such mocks. In this example,
6363
/// 'foo.mocks.dart' will be created.
6464
class MockBuilder implements Builder {
65+
@override
66+
final Map<String, List<String>> buildExtensions = {
67+
'.dart': ['.mocks.dart']
68+
};
69+
70+
MockBuilder({Map<String, List<String>>? buildExtensions}) {
71+
this.buildExtensions.addAll(buildExtensions ?? {});
72+
}
73+
6574
@override
6675
Future<void> build(BuildStep buildStep) async {
6776
if (!await buildStep.resolver.isLibrary(buildStep.inputId)) return;
6877
final entryLib = await buildStep.inputLibrary;
6978
final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
70-
final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
79+
80+
// While it can be acceptable that we get more than 2 allowedOutputs,
81+
// because it's the general one and the user defined one. Having
82+
// more, means that user has conflicting patterns so we should throw.
83+
if (buildStep.allowedOutputs.length > 2) {
84+
throw ArgumentError('Build_extensions has conflicting outputs on file '
85+
'`${buildStep.inputId.path}`, it usually caused by missconfiguration '
86+
'on your `build.yaml` file');
87+
}
88+
// if not single, we always choose the user defined one.
89+
final mockLibraryAsset = buildStep.allowedOutputs.singleOrNull ??
90+
buildStep.allowedOutputs
91+
.where((element) =>
92+
element != buildStep.inputId.changeExtension('.mocks.dart'))
93+
.single;
7194
final inheritanceManager = InheritanceManager3();
7295
final mockTargetGatherer =
7396
_MockTargetGatherer(entryLib, inheritanceManager);
7497

75-
final entryAssetId = await buildStep.resolver.assetIdForElement(entryLib);
7698
final assetUris = await _resolveAssetUris(buildStep.resolver,
77-
mockTargetGatherer._mockTargets, entryAssetId.path, entryLib);
99+
mockTargetGatherer._mockTargets, mockLibraryAsset.path, entryLib);
78100

79101
final mockLibraryInfo = _MockLibraryInfo(mockTargetGatherer._mockTargets,
80102
assetUris: assetUris,
@@ -240,11 +262,6 @@ $rawOutput
240262
}
241263
return element.library!;
242264
}
243-
244-
@override
245-
final buildExtensions = const {
246-
'.dart': ['.mocks.dart']
247-
};
248265
}
249266

250267
/// An [Element] visitor which collects the elements of all of the
@@ -2304,7 +2321,29 @@ class _AvoidConflictsAllocator implements Allocator {
23042321
}
23052322

23062323
/// A [MockBuilder] instance for use by `build.yaml`.
2307-
Builder buildMocks(BuilderOptions options) => MockBuilder();
2324+
Builder buildMocks(BuilderOptions options) {
2325+
final buildExtensions = options.config['build_extensions'];
2326+
if (buildExtensions == null) return MockBuilder();
2327+
if (buildExtensions is! Map) {
2328+
throw ArgumentError(
2329+
'build_extensions should be a map from inputs to outputs');
2330+
}
2331+
final result = <String, List<String>>{};
2332+
for (final entry in buildExtensions.entries) {
2333+
final input = entry.key;
2334+
final output = entry.value;
2335+
if (input is! String || !input.endsWith('.dart')) {
2336+
throw ArgumentError('Invalid key in build_extensions `$input`, it '
2337+
'should be a string ending with `.dart`');
2338+
}
2339+
if (output is! String || !output.endsWith('.mocks.dart')) {
2340+
throw ArgumentError('Invalid key in build_extensions `$output`, it '
2341+
'should be a string ending with `mocks.dart`');
2342+
}
2343+
result[input] = [output];
2344+
}
2345+
return MockBuilder(buildExtensions: result);
2346+
}
23082347

23092348
extension on Element {
23102349
/// Returns the "full name" of a class or method element.

test/builder/auto_mocks_test.dart

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import 'package:mockito/src/builder.dart';
2222
import 'package:package_config/package_config.dart';
2323
import 'package:test/test.dart';
2424

25-
Builder buildMocks(BuilderOptions options) => MockBuilder();
26-
2725
const annotationsAsset = {
2826
'mockito|lib/annotations.dart': '''
2927
class GenerateMocks {
@@ -86,25 +84,27 @@ void main() {
8684

8785
/// Test [MockBuilder] in a package which has not opted into null safety.
8886
Future<void> testPreNonNullable(Map<String, String> sourceAssets,
89-
{Map<String, /*String|Matcher<String>*/ Object>? outputs}) async {
87+
{Map<String, /*String|Matcher<String>*/ Object>? outputs,
88+
Map<String, dynamic> config = const <String, dynamic>{}}) async {
9089
final packageConfig = PackageConfig([
9190
Package('foo', Uri.file('/foo/'),
9291
packageUriRoot: Uri.file('/foo/lib/'),
9392
languageVersion: LanguageVersion(2, 7))
9493
]);
95-
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
94+
await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
9695
writer: writer, outputs: outputs, packageConfig: packageConfig);
9796
}
9897

9998
/// Test [MockBuilder] in a package which has opted into null safety.
10099
Future<void> testWithNonNullable(Map<String, String> sourceAssets,
101-
{Map<String, /*String|Matcher<List<int>>*/ Object>? outputs}) async {
100+
{Map<String, /*String|Matcher<List<int>>*/ Object>? outputs,
101+
Map<String, dynamic> config = const <String, dynamic>{}}) async {
102102
final packageConfig = PackageConfig([
103103
Package('foo', Uri.file('/foo/'),
104104
packageUriRoot: Uri.file('/foo/lib/'),
105105
languageVersion: LanguageVersion(3, 0))
106106
]);
107-
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
107+
await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
108108
writer: writer, outputs: outputs, packageConfig: packageConfig);
109109
}
110110

@@ -3662,6 +3662,114 @@ void main() {
36623662
contains('bar: _FakeBar_0('))));
36633663
});
36643664
});
3665+
3666+
group('build_extensions support', () {
3667+
test('should export mocks to different directory', () async {
3668+
await testWithNonNullable({
3669+
...annotationsAsset,
3670+
...simpleTestAsset,
3671+
'foo|lib/foo.dart': '''
3672+
import 'bar.dart';
3673+
class Foo extends Bar {}
3674+
''',
3675+
'foo|lib/bar.dart': '''
3676+
import 'dart:async';
3677+
class Bar {
3678+
m(Future<void> a) {}
3679+
}
3680+
''',
3681+
}, config: {
3682+
"build_extensions": {"^test/{{}}.dart": "test/mocks/{{}}.mocks.dart"}
3683+
});
3684+
final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
3685+
final mocksContent = utf8.decode(writer.assets[mocksAsset]!);
3686+
expect(mocksContent, contains("import 'dart:async' as _i3;"));
3687+
expect(mocksContent, contains('m(_i3.Future<void>? a)'));
3688+
});
3689+
3690+
test('should throw if it has confilicting outputs', () async {
3691+
await expectLater(
3692+
testWithNonNullable({
3693+
...annotationsAsset,
3694+
...simpleTestAsset,
3695+
'foo|lib/foo.dart': '''
3696+
import 'bar.dart';
3697+
class Foo extends Bar {}
3698+
''',
3699+
'foo|lib/bar.dart': '''
3700+
import 'dart:async';
3701+
class Bar {
3702+
m(Future<void> a) {}
3703+
}
3704+
''',
3705+
}, config: {
3706+
"build_extensions": {
3707+
"^test/{{}}.dart": "test/mocks/{{}}.mocks.dart",
3708+
"test/{{}}.dart": "test/{{}}.something.mocks.dart"
3709+
}
3710+
}),
3711+
throwsArgumentError);
3712+
final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
3713+
final otherMocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
3714+
final somethingMocksAsset =
3715+
AssetId('foo', 'test/mocks/foo_test.something.mocks.dart');
3716+
3717+
expect(writer.assets.containsKey(mocksAsset), false);
3718+
expect(writer.assets.containsKey(otherMocksAsset), false);
3719+
expect(writer.assets.containsKey(somethingMocksAsset), false);
3720+
});
3721+
3722+
test('should throw if input is in incorrect format', () async {
3723+
await expectLater(
3724+
testWithNonNullable({
3725+
...annotationsAsset,
3726+
...simpleTestAsset,
3727+
'foo|lib/foo.dart': '''
3728+
import 'bar.dart';
3729+
class Foo extends Bar {}
3730+
''',
3731+
'foo|lib/bar.dart': '''
3732+
import 'dart:async';
3733+
class Bar {
3734+
m(Future<void> a) {}
3735+
}
3736+
''',
3737+
}, config: {
3738+
"build_extensions": {"^test/{{}}": "test/mocks/{{}}.mocks.dart"}
3739+
}),
3740+
throwsArgumentError);
3741+
final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
3742+
final mocksAssetOriginal = AssetId('foo', 'test/foo_test.mocks.dart');
3743+
3744+
expect(writer.assets.containsKey(mocksAsset), false);
3745+
expect(writer.assets.containsKey(mocksAssetOriginal), false);
3746+
});
3747+
3748+
test('should throw if output is in incorrect format', () async {
3749+
await expectLater(
3750+
testWithNonNullable({
3751+
...annotationsAsset,
3752+
...simpleTestAsset,
3753+
'foo|lib/foo.dart': '''
3754+
import 'bar.dart';
3755+
class Foo extends Bar {}
3756+
''',
3757+
'foo|lib/bar.dart': '''
3758+
import 'dart:async';
3759+
class Bar {
3760+
m(Future<void> a) {}
3761+
}
3762+
''',
3763+
}, config: {
3764+
"build_extensions": {"^test/{{}}.dart": "test/mocks/{{}}.g.dart"}
3765+
}),
3766+
throwsArgumentError);
3767+
final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
3768+
final mocksAssetOriginal = AssetId('foo', 'test/foo_test.mocks.dart');
3769+
expect(writer.assets.containsKey(mocksAsset), false);
3770+
expect(writer.assets.containsKey(mocksAssetOriginal), false);
3771+
});
3772+
});
36653773
}
36663774

36673775
TypeMatcher<List<int>> _containsAllOf(a, [b]) => decodedMatches(

0 commit comments

Comments
 (0)