The expect_error
package is a testing library inspired by Typescript's
// @expect-error
, designed to help package authors to test compilation errors.
expect_error
exposes a compiles
matcher, which can be used within unit tests.
A simple example would be:
// test/my_test.dart
import 'package:expect_error/expect_error.dart';
import 'package:test/test.dart';
void main() async {
final library = await Library.parseFromStacktrace();
test('String is not assignable to int', () async {
await expectLater(library.withCode('''
// expect-error: INVALID_ASSIGNMENT
int value = "string";
'''), compiles);
});
}
This example tests that the code:
int value = "string";
emits the compilation error "INVALID_ASSIGNMENT".
You may wonder why:
await expectLater(library.withCode('''
// expect-error: INVALID_ASSIGNMENT
int value = "string";
'''), compiles);
is preferrable to:
await expectLater(library.withCode('''
int value = "string";
'''), throwsCompilationError('INVALID_ASSIGNMENT'));
The reason why expect_error
relies on a comment is because a comment
doesn't simply communicate what the error is, but also where that error is.
When using // expect-error: x
, only the next line is allowed to emit the
compilation error.
As such, if we do:
await expectLater(library.withCode('''
void main() {
// expect-error: INVALID_ASSIGNMENT
print('a');
int value = "string";
}
'''), compiles);
Then this test will fail. Because while the code block indeed contains an
INVALID_ASSIGMENT
error, the error isn't on the print
but instead on
the int value = 'string'
.
To fix out test, we would have to move the comment on the line before the error:
await expectLater(library.withCode('''
void main() {
print('a');
// expect-error: INVALID_ASSIGNMENT
int value = "string";
}
'''), compiles);
It is possible for expect-error
to specify multiple codes at once by separating
them with a ,
:
await expectLater(library.withCode(r'''
String fn(int a) => '';
// expect-error: NOT_ENOUGH_POSITIONAL_ARGUMENTS, INVALID_ASSIGNMENT
int a = fn();
'''), compiles);
It is possible to use import
directives to import dart code within code blocks:
await expectLater(library.withCode(r'''
import 'package:riverpod/riverpod.dart';
final provider = Provider<int>((ref) {
// expect-error: INVALID_ASSIGNMENT
return 'string';
});
'''), compiles);
The imports available within our code blocks are dependent on that library
variable.
When we do:
void main() async {
final library = await Library.parseFromStacktrace();
test('...', () async {
await expectLater(library.withCode(...), compiles);
});
}
that library
variable we created tells expect_error
what imports
the tested code block can use.
In particular, Library.parseFromStacktrace()
makes our tested code behave as if
it was defined in a separate file within the same folder our test file.
As such, the following code is valid too:
import 'package:expect_error/expect_error.dart';
import 'package:test/test.dart';
// our test file can use relative import to import another file
import 'relative.dart';
void main() async {
final library = await Library.parseFromStacktrace();
test('...', () async {
await expectLater(library.withCode('''
// our tested code can also import the same file
import 'relative.dart';
'''), compiles);
});
}
Unfortunately, since expect_error
is built on top of the analyzer
package, it means that a package cannot both depend on Flutter and expect_error
at
the same time.
As such, to use expect_error
to test compilation errors when interacting with
Flutter, a workaround is necessary.
The solution is to move your tests that depends on Flutter in a separate package
used only for test purpose, with no dependency on Flutter.
As example, consider a package my_package
that depends on Flutter, which exposes a
MyWidget
class:
// my_package/lib/my_widget.dart
import 'package:flutter/material.dart';
class MyWidget extends StatelessWidget {
const MyWidget({Key? key, required String parameter}) : super(key: key);
...
}
To test this MyWidget
class, rather than adding our test
within the my_package/test
folder, we could create a new Dart project
such that our folder architecture looks like:
my_package
puspec.yaml
lib
my_widget.dart
expect_error_test
pubspec.yaml
test
my_widget_test.dart
This expect_error_test
app would depend on expect_error
:
name: expect_error_test
---
dev_dependencies:
expect_error: ...
Then, within expect_error_test/test/my_widget_test
, we could do:
void main() {
final flutterLibrary = await Library.custom(
packageName: 'my_package', // the name of the package that contains this code block
packageRoot: '..', // the path to the root of this package
path: 'test/my_test.dart', // where the codeblock is located within the package
);
await expectLater(flutterLibrary.withCode(r'''
import 'package:my_package/my_widget.dart';
void main() {
// expect-error: MISSING_REQUIRED_ARGUMENT
MyWidget();
}
'''), compiles);
}
Notice how rather than Library.parseFromStacktrace()
we used Library.custom(...)
.
By doing so, rather than assuming that our code block is within the same
folder as our test, we were able to make it behave as if our code block was
within my_package
.
This way, our tests using expect_error
are correctly able to import Flutter code.