Skip to content

Commit e54a006

Browse files
Merge pull request #685 from LuisDuarte1:feature/build-extensions
PiperOrigin-RevId: 558114686
2 parents ff79de6 + 5f3a4ca commit e54a006

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,39 @@ 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+
If you specify a build extension, you **MUST** ensure that your patterns cover all input files that you want generate mocks from. Failing to do so will lead to the unmatched file from not being generated at all.
188+
189+
```yaml
190+
targets:
191+
$default:
192+
builders:
193+
mockito|mockBuilder:
194+
generate_for:
195+
options:
196+
# build_extensions takes a source pattern and if it matches it will transform the output
197+
# to your desired path. The default behaviour is to the .mocks.dart file to be in the same
198+
# directory as the source .dart file. As seen below this is customizable, but the generated
199+
# file must always end in `.mocks.dart`.
200+
build_extensions:
201+
'^tests/{{}}.dart' : 'tests/mocks/{{}}.mocks.dart'
202+
'^integration-tests/{{}}.dart' : 'integration-tests/{{}}.mocks.dart'
203+
```
204+
205+
Also, you can also check out the example configuration in the Mockito repository.

build.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ 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+
#
14+
# If you specify custom build_extensions you MUST ensure that they cover all input files
15+
build_extensions:
16+
'^example/build_extensions/{{}}.dart' : 'example/build_extensions/mocks/{{}}.mocks.dart'
17+
'^example/example.dart' : 'example/example.mocks.dart'
18+
'^example/iss/{{}}.dart' : 'example/iss/{{}}.mocks.dart'
19+
'^test/end2end/{{}}.dart' : 'test/end2end/{{}}.mocks.dart'
820

921
builders:
1022
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: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,34 @@ 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+
68+
const MockBuilder(
69+
{this.buildExtensions = const {
70+
'.dart': ['.mocks.dart']
71+
}});
72+
6573
@override
6674
Future<void> build(BuildStep buildStep) async {
6775
if (!await buildStep.resolver.isLibrary(buildStep.inputId)) return;
6876
final entryLib = await buildStep.inputLibrary;
6977
final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
70-
final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
78+
79+
final mockLibraryAsset = buildStep.allowedOutputs.singleOrNull;
80+
if (mockLibraryAsset == null) {
81+
throw ArgumentError(
82+
'Build_extensions has missing or conflicting outputs for '
83+
'`${buildStep.inputId.path}`, this is usually caused by a misconfigured '
84+
'build extension override in `build.yaml`');
85+
}
86+
7187
final inheritanceManager = InheritanceManager3();
7288
final mockTargetGatherer =
7389
_MockTargetGatherer(entryLib, inheritanceManager);
7490

75-
final entryAssetId = await buildStep.resolver.assetIdForElement(entryLib);
7691
final assetUris = await _resolveAssetUris(buildStep.resolver,
77-
mockTargetGatherer._mockTargets, entryAssetId.path, entryLib);
92+
mockTargetGatherer._mockTargets, mockLibraryAsset.path, entryLib);
7893

7994
final mockLibraryInfo = _MockLibraryInfo(mockTargetGatherer._mockTargets,
8095
assetUris: assetUris,
@@ -240,11 +255,6 @@ $rawOutput
240255
}
241256
return element.library!;
242257
}
243-
244-
@override
245-
final buildExtensions = const {
246-
'.dart': ['.mocks.dart']
247-
};
248258
}
249259

250260
/// An [Element] visitor which collects the elements of all of the
@@ -2304,7 +2314,29 @@ class _AvoidConflictsAllocator implements Allocator {
23042314
}
23052315

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

23092341
extension on Element {
23102342
/// 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)