我的目标是让我的单元测试易于理解。目前,它们很难理解,因为它们有太多嵌套函数。
我想使用 build_runner 生成单元的代码,并解开所有函数。
这是我当前测试的一个例子:
测试.dart
import 'package:example_usage/src/unwrap.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class Cat {
String sound() => "Meow";
int walk() => 4;
}
class Dog {
final Cat cat;
Dog(this.cat);
String sayHi() {
return this.cat.sound();
}
int jump() {
return this.cat.walk();
}
}
class MockCat extends Mock implements Cat {}
void main() {
MockCat cat;
Dog dog;
@UnWrap()
void setupCatSoundStub() {
when(cat.sound()).thenReturn("Woof");
}
@UnWrap()
void setupCatWalkstub() {
when(cat.walk()).thenReturn(2);
}
@UnWrap()
void expectCatCalled() {
verify(cat.sound());
}
@UnWrap()
void testDogWoof() {
setupCatSoundStub();
dog = Dog(cat);
final sound = dog.sayHi();
expect(sound, "Woof");
expectCatCalled();
}
void expectCatWalked() {
verify(cat.walk());
}
group('Dog Cat Play', () {
setUp(() {
cat = MockCat();
});
test('Dog woof', () {
testDogWoof();
});
test('Dog woof then jump', () {
testDogWoof();
setupCatWalkstub();
final steps = dog.jump();
expect(steps, 2);
expectCatWalked();
});
});
}
我想生成这样的代码
_$test.dart
void _$main() {
MockCat cat;
Dog dog;
void expectCatWalked() {
verify(cat.walk());
}
group('Dog Cat Play', () {
setUp(() {
cat = MockCat();
});
test('Dog woof', () {
// testDogWoof();
// setupCatSoundStub();
when(cat.sound()).thenReturn("Woof");
dog = Dog(cat);
final sound = dog.sayHi();
expect(sound, "Woof");
// expectCatCalled();
verify(cat.sound());
});
test('Dog woof then jump', () {
// testDogWoof();
// setupCatSoundStub();
when(cat.sound()).thenReturn("Woof");
dog = Dog(cat);
final sound = dog.sayHi();
expect(sound, "Woof");
// expectCatCalled();
verify(cat.sound());
// setupCatWalkstub();
when(cat.walk()).thenReturn(2);
final steps = dog.jump();
expect(steps, 2);
expectCatWalked();
});
});
}
我在网上找到了一些教程,但我可以找到有关将函数体转换为字符串的文档(有些像 JavaScript 的
Function.prototype.toString()
方法)我是代码生成新手,所以我尝试打印所有字段,但我找不到类似的内容。
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
class InfoGenerator extends Generator {
@override
FutureOr<String> generate(LibraryReader library, BuildStep buildStep) {
var buffer = StringBuffer();
// library.allElements.forEach((element) {
// buffer.writeln(
// '// ${element.displayName} - ${element.source.fullName} - ${element.declaration}');
// });
library.allElements.whereType<TopLevelVariableElement>().forEach((element) {
buffer.writeln('/*');
buffer.writeln(element.toString());
buffer.writeln('*/');
buffer.writeln(
'// ${element.name} - ${element.kind.displayName} - ${element.declaration}');
});
return buffer.toString();
}
}
我对注释也是新手,所以我只是做了这个
/// What to do here ?
class UnWrap {
const UnWrap();
}
我正在尝试做的事情可能吗?
您尝试做的事情确实是可能的,但它需要一种更复杂的方法,使用分析器包来解析和操作 Dart 代码的抽象语法树(AST)。让我们一步步分解问题并创建解决方案。
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
class UnWrapGenerator extends GeneratorForAnnotation<UnWrap> {
@override
String generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
if (element is FunctionElement) {
final functionBody = _getFunctionBody(element);
return '// Unwrapped ${element.name}:\n$functionBody\n';
}
return '';
}
String _getFunctionBody(FunctionElement element) {
final result = element.session.getParsedLibraryByElement(element.library);
if (result is ParsedLibraryResult) {
final functionDeclaration = result.getElementDeclaration(element);
if (functionDeclaration != null) {
final functionNode = functionDeclaration.node as FunctionDeclaration;
final body = functionNode.functionExpression.body;
if (body is BlockFunctionBody) {
return body.block.toSource();
}
}
}
return '// Unable to retrieve function body';
}
}
Builder unwrapGenerator(BuilderOptions options) =>
LibraryBuilder(UnWrapGenerator(), generatedExtension: '.unwrap.dart');
该生成器执行以下操作:
它扩展了 GeneratorForAnnotation,这意味着它只会处理用 @UnWrap() 注解的元素。 在generateForAnnotatedElement方法中,它检查带注释的元素是否是一个函数。 如果它是一个函数,它会调用 _getFunctionBody 来检索函数的主体。 _getFunctionBody 方法使用分析器解析库并找到函数声明,然后将函数体提取为字符串。
现在,您需要设置 build.yaml 文件才能使用此生成器:
targets:
$default:
builders:
your_package_name|unwrap:
enabled: true
builders:
unwrap:
import: "package:your_package_name/builder.dart"
builder_factories: ["unwrapGenerator"]
build_extensions: {".dart": [".unwrap.dart"]}
auto_apply: dependents
build_to: source
您的包中还需要一个 builder.dart 文件:
library unwrap_builder;
导入'package:build/build.dart'; 导入'src/unwrap_generator.dart';
Builder unwrapGenerator(BuilderOptions 选项) => LibraryBuilder(UnWrapGenerator(), generatedExtension: '.unwrap.dart');
通过此设置,当您运行 pub run build_runner build 时,它将为每个包含 @UnWrap() 注释的文件生成一个新文件。生成的文件将包含解包的函数体。 要在测试中使用这些生成的函数,您可以导入生成的文件并直接调用解包的函数。以下是修改测试文件的方法:
import 'package:example_usage/src/unwrap.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'test.unwrap.dart'; // Import the generated file
class Cat {
String sound() => "Meow";
int walk() => 4;
}
class Dog {
final Cat cat;
Dog(this.cat);
String sayHi() {
return this.cat.sound();
}
int jump() {
return this.cat.walk();
}
}
class MockCat extends Mock implements Cat {}
void main() {
MockCat cat;
Dog dog;
void expectCatWalked() {
verify(cat.walk());
}
group('Dog Cat Play', () {
setUp(() {
cat = MockCat();
});
test('Dog woof', () {
// Use the generated unwrapped functions
setupCatSoundStub(cat);
dog = Dog(cat);
final sound = dog.sayHi();
expect(sound, "Woof");
expectCatCalled(cat);
});
test('Dog woof then jump', () {
setupCatSoundStub(cat);
dog = Dog(cat);
final sound = dog.sayHi();
expect(sound, "Woof");
expectCatCalled(cat);
setupCatWalkstub(cat);
final steps = dog.jump();
expect(steps, 2);
expectCatWalked();
});
});
}
这种方法允许您保持测试的可读性和可维护性,同时在需要时仍然可以访问未包装的函数体。生成的文件将包含每个未包装函数的完整实现,从而可以轻松准确地查看每个测试用例中发生的情况。 每当您更改 @UnWrap() 带注释的函数时,请记住运行 pub run build_runner build 以重新生成未包装的版本。
当我在 2021 年第一次发布这篇文章时,我也遇到了同样的情况——努力将函数体提取为
String
。
冒险进入代码生成器领域并不适合胆小的人。这是一项具有挑战性的工作,比使用
GetX
等镀银工具管理状态或处理 Flutter's
导航的复杂性要复杂得多。它使得驯服 MediaQuery
或与 Flexible
和 Expanded
小部件战斗看起来就像在公园散步一样轻松。
因此,我在这里分享一个单一的工作项目,值得花费数小时的努力和无数的挣扎,以帮助任何人。
https://github.com/tolotrasamuel/func_unwrapper
Block? getFuncBodyFromFuncDeclaration(FunctionDeclaration astNode) {
final funcExpression =
astNode.childEntities.whereType<FunctionExpression>().firstOrNull;
final blockFuncBody = funcExpression?.childEntities
.whereType<BlockFunctionBody>()
.firstOrNull;
final block = blockFuncBody?.childEntities.whereType<Block>().firstOrNull;
// final blockExpression = block.childEntities.whereType<ExpressionStatement>().first;
return block;
}
可以直接使用,
Block
类有一个函数或者getter可以直接获取函数体,如String
。但或者,它还包含函数的第一个和最后一个字符相对于原始源文件的偏移量。
static Selector getBlockSelector(Block withElementWithOriginalOffset) {
final statements = withElementWithOriginalOffset.childEntities
.where((e) => (e is AstNode));
if (statements.isEmpty) return Selector(0, 0);
return Selector(statements.first.offset, statements.last.end);
}
查看这一行以了解如何将原始源文件读取为字符串。
我更喜欢使用第二个选项,因为它更容易达到我的目的。
Future<String?> readAsString(AssetId inputId) async {
try {
return await buildStep.readAsString(inputId);
} catch (e, trace) {
print('Possible BuildStepCompletedException $e');
print(trace);
return null;
}
}
祝你好运!