blob: 059851fc79f023e19c974a05b5fb5aa608d5b0d5 [file] [log] [blame]
// Copyright 2020 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.
// @dart=2.9
@TestOn('vm')
import 'dart:convert' show utf8;
import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:meta/meta.dart';
import 'package:mockito/src/builder.dart';
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 {
final List<Type> classes;
final List<MockSpec> customMocks;
const GenerateMocks(this.classes, {this.customMocks = []});
}
class MockSpec<T> {
final Symbol mockName;
final bool returnNullOnMissingStub;
const MockSpec({Symbol as, this.returnNullOnMissingStub = false})
: mockName = as;
}
'''
};
const mockitoAssets = {
'mockito|lib/mockito.dart': '''
export 'src/mock.dart';
''',
'mockito|lib/src/mock.dart': '''
class Mock {}
'''
};
const simpleTestAsset = {
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>()])
void main() {}
'''
};
const _constructorWithThrowOnMissingStub = '''
MockFoo() {
_i1.throwOnMissingStub(this);
}''';
void main() {
InMemoryAssetWriter writer;
/// Test [MockBuilder] in a package which has not opted into null safety.
Future<void> testPreNonNullable(Map<String, String> sourceAssets,
{Map<String, /*String|Matcher<String>*/ dynamic> outputs}) async {
var packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(2, 7))
]);
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
outputs: outputs, packageConfig: packageConfig);
}
/// Builds with [MockBuilder] in a package which has opted into null safety,
/// returning the content of the generated mocks library.
Future<String> buildWithNonNullable(Map<String, String> sourceAssets) async {
var packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(2, 12))
]);
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
writer: writer, packageConfig: packageConfig);
var mocksAsset = AssetId('foo', 'test/foo_test.mocks.dart');
return utf8.decode(writer.assets[mocksAsset]);
}
setUp(() {
writer = InMemoryAssetWriter();
});
test('generates a generic mock class without type arguments', () async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo<T> {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(as: #MockFoo)])
void main() {}
'''
});
expect(mocksContent,
contains('class MockFoo<T> extends _i1.Mock implements _i2.Foo<T>'));
});
test('generates a generic mock class with type arguments', () async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo<T, U> {}
class Bar {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks(
[], customMocks: [MockSpec<Foo<int, Bar>>(as: #MockFooOfIntBar)])
void main() {}
'''
});
expect(
mocksContent,
contains(
'class MockFooOfIntBar extends _i1.Mock implements _i2.Foo<int, _i2.Bar>'));
});
test('generates a generic mock class with type arguments but no name',
() async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo<T> {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo<int>>()])
void main() {}
'''
});
expect(mocksContent,
contains('class MockFoo extends _i1.Mock implements _i2.Foo<int>'));
});
test('generates a generic, bounded mock class without type arguments',
() async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo<T extends Object> {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(as: #MockFoo)])
void main() {}
'''
});
expect(
mocksContent,
contains(
'class MockFoo<T extends Object> extends _i1.Mock implements _i2.Foo<T>'));
});
test('generates mock classes from multiple annotations', () async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo {}
class Bar {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>()])
void fooTests() {}
@GenerateMocks([], customMocks: [MockSpec<Bar>()])
void barTests() {}
'''
});
expect(mocksContent,
contains('class MockFoo extends _i1.Mock implements _i2.Foo'));
expect(mocksContent,
contains('class MockBar extends _i1.Mock implements _i2.Bar'));
});
test('generates mock classes from multiple annotations on a single element',
() async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/a.dart': dedent(r'''
class Foo {}
'''),
'foo|lib/b.dart': dedent(r'''
class Foo {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/a.dart' as a;
import 'package:foo/b.dart' as b;
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<a.Foo>(as: #MockAFoo)])
@GenerateMocks([], customMocks: [MockSpec<b.Foo>(as: #MockBFoo)])
void main() {}
'''
});
expect(mocksContent,
contains('class MockAFoo extends _i1.Mock implements _i2.Foo'));
expect(mocksContent,
contains('class MockBFoo extends _i1.Mock implements _i3.Foo'));
});
test(
'generates a mock class which uses the old behavior of returning null on '
'missing stubs', () async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo<T> {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(as: #MockFoo, returnNullOnMissingStub: true)])
void main() {}
'''
});
expect(mocksContent, isNot(contains('throwOnMissingStub')));
});
test(
'throws when GenerateMocks is given a class with a type parameter with a '
'private bound', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo<T extends _Bar> {
void m(int a) {}
}
class _Bar {}
'''),
'foo|test/foo_test.dart': dedent('''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>()])
void main() {}
'''),
},
message: contains(
"The class 'Foo' features a private type parameter bound, and cannot "
'be stubbed.'),
);
});
test('throws when MockSpec() is missing a type argument', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|test/foo_test.dart': dedent('''
import 'package:mockito/annotations.dart';
// Missing required type argument to MockSpec.
@GenerateMocks([], customMocks: [MockSpec()])
void main() {}
'''),
},
message: contains('Mockito cannot mock `dynamic`'),
);
});
test('throws when MockSpec uses a private class', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|test/foo_test.dart': dedent('''
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<_Foo>()])
void main() {}
class _Foo {}
'''),
},
message: contains('Mockito cannot mock a private type: _Foo.'),
);
});
test('throws when two distinct classes with the same name are mocked',
() async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|lib/a.dart': dedent(r'''
class Foo {}
'''),
'foo|lib/b.dart': dedent(r'''
class Foo {}
'''),
'foo|test/foo_test.dart': dedent('''
import 'package:foo/a.dart' as a;
import 'package:foo/b.dart' as b;
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<a.Foo>()])
@GenerateMocks([], customMocks: [MockSpec<b.Foo>()])
void main() {}
'''),
},
message: contains(
'Mockito cannot generate two mocks with the same name: MockFoo (for '
'Foo declared in /foo/lib/a.dart, and for Foo declared in '
'/foo/lib/b.dart)'),
);
});
test('throws when a mock class of the same name already exists', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
class Foo {}
'''),
'foo|test/foo_test.dart': dedent('''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>()])
void main() {}
class MockFoo {}
'''),
},
message: contains(
'Mockito cannot generate a mock with a name which conflicts with '
'another class declared in this library: MockFoo'),
);
});
test('throws when a mock class of class-to-mock already exists', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
...mockitoAssets,
'foo|lib/foo.dart': dedent(r'''
class Foo {}
'''),
'foo|test/foo_test.dart': dedent('''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>()])
void main() {}
class FakeFoo extends Mock implements Foo {}
'''),
},
message: contains(
'contains a class which appears to already be mocked inline: FakeFoo'),
);
});
test('throws when MockSpec references a typedef', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
...simpleTestAsset,
'foo|lib/foo.dart': dedent(r'''
typedef Foo = void Function();
'''),
},
message: 'Mockito cannot mock a typedef: Foo',
);
});
test('throws when MockSpec references an enum', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
...simpleTestAsset,
'foo|lib/foo.dart': dedent(r'''
enum Foo {}
'''),
},
message: 'Mockito cannot mock an enum: Foo',
);
});
test('throws when MockSpec references a non-subtypeable type', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|test/foo_test.dart': dedent('''
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<int>()])
void main() {}
'''),
},
message: contains('Mockito cannot mock a non-subtypable type: int'),
);
});
test('given a pre-non-nullable library, does not override any members',
() async {
await testPreNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
'foo|lib/foo.dart': dedent(r'''
abstract class Foo {
int f(int a);
}
'''),
},
outputs: {
'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
class MockFoo extends _i1.Mock implements _i2.Foo {
$_constructorWithThrowOnMissingStub
}
'''))
},
);
});
}
TypeMatcher<List<int>> _containsAllOf(a, [b]) => decodedMatches(
b == null ? allOf(contains(a)) : allOf(contains(a), contains(b)));
/// Expect that [testBuilder], given [assets], throws an
/// [InvalidMockitoAnnotationException] with a message containing [message].
void _expectBuilderThrows(
{@required Map<String, String> assets,
@required dynamic /*String|Matcher<List<int>>*/ message}) {
expect(
() async => await testBuilder(buildMocks(BuilderOptions({})), assets),
throwsA(TypeMatcher<InvalidMockitoAnnotationException>()
.having((e) => e.message, 'message', message)));
}
/// Dedent [input], so that each line is shifted to the left, so that the first
/// line is at the 0 column.
String dedent(String input) {
final indentMatch = RegExp(r'^(\s*)').firstMatch(input);
final indent = ''.padRight(indentMatch.group(1).length);
return input.splitMapJoin('\n',
onNonMatch: (s) => s.replaceFirst(RegExp('^$indent'), ''));
}