Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

Commit 7db4f86

Browse files
authored
feat: add static code diagnostic missing-test-assertion (#1023)
1 parent b8353bb commit 7db4f86

File tree

9 files changed

+377
-0
lines changed

9 files changed

+377
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 4.21.0
4+
5+
* feat: add static code diagnostic [`missing-test-assertion`](https://dartcodemetrics.dev/docs/rules/common/missing-test-assertion).
6+
37
## 4.20.0
48

59
* feat: add logger and progress indication.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
part of 'missing_test_assertion_rule.dart';
2+
3+
class _ConfigParser {
4+
static const _includeAssertionsConfig = 'include-assertions';
5+
6+
static Iterable<String> parseIncludeAssertions(Map<String, Object> config) =>
7+
config.containsKey(_includeAssertionsConfig) &&
8+
config[_includeAssertionsConfig] is Iterable
9+
? List<String>.from(config[_includeAssertionsConfig] as Iterable)
10+
: <String>[];
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// ignore_for_file: public_member_api_docs
2+
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer/dart/ast/visitor.dart';
5+
6+
import '../../../../../utils/node_utils.dart';
7+
import '../../../lint_utils.dart';
8+
import '../../../models/internal_resolved_unit_result.dart';
9+
import '../../../models/issue.dart';
10+
import '../../../models/severity.dart';
11+
import '../../models/common_rule.dart';
12+
import '../../rule_utils.dart';
13+
14+
part 'config_parser.dart';
15+
part 'visitor.dart';
16+
17+
class MissingTestAssertionRule extends CommonRule {
18+
static const String ruleId = 'missing-test-assertion';
19+
20+
static const _warningMessage = 'Missing test assertion.';
21+
22+
final Iterable<String> _includeAssertions;
23+
24+
MissingTestAssertionRule([Map<String, Object> config = const {}])
25+
: _includeAssertions = _ConfigParser.parseIncludeAssertions(config),
26+
super(
27+
id: ruleId,
28+
severity: readSeverity(config, Severity.warning),
29+
excludes:
30+
hasExcludes(config) ? readExcludes(config) : ['lib/**', 'bin/**'],
31+
);
32+
33+
@override
34+
Iterable<Issue> check(InternalResolvedUnitResult source) {
35+
final visitor = _Visitor(_includeAssertions);
36+
37+
source.unit.visitChildren(visitor);
38+
39+
return visitor.nodes
40+
.map((node) => createIssue(
41+
rule: this,
42+
location: nodeLocation(node: node, source: source),
43+
message: _warningMessage,
44+
))
45+
.toList(growable: false);
46+
}
47+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
part of 'missing_test_assertion_rule.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final _nodes = <AstNode>[];
5+
6+
Iterable<AstNode> get nodes => _nodes;
7+
8+
final Iterable<String> _includeAssertions;
9+
10+
_Visitor(this._includeAssertions);
11+
12+
@override
13+
void visitFunctionDeclaration(FunctionDeclaration node) {
14+
super.visitFunctionDeclaration(node);
15+
if (node.name.toString() != 'main') {
16+
return;
17+
}
18+
19+
final visitor = _MethodTestVisitor(_includeAssertions);
20+
node.visitChildren(visitor);
21+
_nodes.addAll(visitor.nodes);
22+
}
23+
}
24+
25+
class _MethodTestVisitor extends RecursiveAstVisitor<void> {
26+
static const _testMethodNameList = <String>{
27+
'test',
28+
'testWidgets',
29+
};
30+
31+
final _nodes = <AstNode>[];
32+
33+
Iterable<AstNode> get nodes => _nodes;
34+
35+
final Iterable<String> _includeAssertions;
36+
37+
_MethodTestVisitor(this._includeAssertions);
38+
39+
@override
40+
void visitMethodInvocation(MethodInvocation node) {
41+
super.visitMethodInvocation(node);
42+
if (!_testMethodNameList.contains(node.methodName.toString())) {
43+
return;
44+
}
45+
46+
final visitor = _MethodAssertionVisitor(_includeAssertions);
47+
node.visitChildren(visitor);
48+
if (visitor.hasContainedAssertion) {
49+
return;
50+
}
51+
52+
_nodes.add(node);
53+
}
54+
}
55+
56+
class _MethodAssertionVisitor extends RecursiveAstVisitor<void> {
57+
final _assertionMethodNameList = <String>{
58+
'expect',
59+
'expectAsync0',
60+
'expectAsync1',
61+
'expectAsync2',
62+
'expectAsync3',
63+
'expectAsync4',
64+
'expectAsync5',
65+
'expectAsync6',
66+
'expectAsyncUntil0',
67+
'expectAsyncUntil1',
68+
'expectAsyncUntil2',
69+
'expectAsyncUntil3',
70+
'expectAsyncUntil4',
71+
'expectAsyncUntil5',
72+
'expectAsyncUntil6',
73+
'expectLater',
74+
};
75+
76+
final Iterable<String> _includeAssertions;
77+
78+
_MethodAssertionVisitor(this._includeAssertions) {
79+
_assertionMethodNameList.addAll(_includeAssertions);
80+
}
81+
82+
bool hasContainedAssertion = false;
83+
84+
@override
85+
void visitMethodInvocation(MethodInvocation node) {
86+
super.visitMethodInvocation(node);
87+
if (_assertionMethodNameList.contains(node.methodName.name)) {
88+
hasContainedAssertion = true;
89+
}
90+
}
91+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
void main() {
2+
print('Hello');
3+
4+
test('good unit test', () {
5+
final a = 1;
6+
final b = 2;
7+
final c = a + 1;
8+
expect(b, c);
9+
});
10+
11+
testWidgets('good widget test', (WidgetTester tester) async {
12+
await tester.pumpWidget(MyApp());
13+
await tester.pumpAndSettle();
14+
expect(find.text('Welcome'), findsOneWidget);
15+
});
16+
17+
test('with expectLater', () {
18+
await expectLater(1, 1);
19+
});
20+
21+
test('with expectAsync0', () {
22+
expectAsync0(() {});
23+
});
24+
25+
test('with expectAsync1', () {
26+
expectAsync1((p0) {});
27+
});
28+
29+
test('with expectAsync1', () {
30+
expectAsync2((p0, p1) {});
31+
});
32+
33+
test('with expectAsync3', () {
34+
expectAsync3((p0, p1, p2) {});
35+
});
36+
37+
test('with expectAsync4', () {
38+
expectAsync4((p0, p1, p2, p3) {});
39+
});
40+
41+
test('with expectAsync5', () {
42+
expectAsync5((p0, p1, p2, p3, p4) {});
43+
});
44+
45+
test('with expectAsync6', () {
46+
expectAsync6((p0, p1, p2, p3, p4, p5) {});
47+
});
48+
49+
test('with expectAsyncUntil0', () {
50+
expectAsyncUntil0(() {}, () => true);
51+
});
52+
53+
test('with expectAsyncUntil1', () {
54+
expectAsyncUntil1((p0) {}, () => true);
55+
});
56+
57+
test('with expectAsyncUntil2', () {
58+
expectAsyncUntil2((p0, p1) {}, () => true);
59+
});
60+
61+
test('with expectAsyncUntil3', () {
62+
expectAsyncUntil3((p0, p1, p2) {}, () => true);
63+
});
64+
65+
test('with expectAsyncUntil4', () {
66+
expectAsyncUntil4((p0, p1, p2, p3) {}, () => true);
67+
});
68+
69+
test('with expectAsyncUntil5', () {
70+
expectAsyncUntil5((p0, p1, p2, p3, p4) {}, () => true);
71+
});
72+
73+
test('with expectAsyncUntil6', () {
74+
expectAsyncUntil6((p0, p1, p2, p3, p4, p5) {}, () => true);
75+
});
76+
77+
test(null, () => expect(1, 1));
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
void main() {
2+
print('Hello');
3+
4+
test(null, () => verify(1, 1));
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
void main() {
2+
print('Hello');
3+
4+
test('bad unit test', () {
5+
final a = 1;
6+
final b = 2;
7+
final c = a + 1;
8+
}); // LINT
9+
10+
testWidgets('bad widget test', (WidgetTester tester) async {
11+
await tester.pumpWidget(MyApp());
12+
await tester.pumpAndSettle();
13+
}); // LINT
14+
15+
test(null, () => 1 == 1); // LINT
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
2+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/missing_test_assertion/missing_test_assertion_rule.dart';
3+
import 'package:test/test.dart';
4+
5+
import '../../../../../helpers/rule_test_helper.dart';
6+
7+
const _correctExamplePath =
8+
'missing_test_assertion/examples/correct_example.dart';
9+
const _incorrectExamplePath =
10+
'missing_test_assertion/examples/incorrect_example.dart';
11+
const _customAssertionsExamplePath =
12+
'missing_test_assertion/examples/custom_assertions_example.dart';
13+
14+
void main() {
15+
group('MissingTestAssertion', () {
16+
test('initialization', () async {
17+
final unit = await RuleTestHelper.resolveFromFile(_correctExamplePath);
18+
final issues = MissingTestAssertionRule().check(unit);
19+
20+
RuleTestHelper.verifyInitialization(
21+
issues: issues,
22+
ruleId: 'missing-test-assertion',
23+
severity: Severity.warning,
24+
);
25+
});
26+
27+
test('with default config reports no issues', () async {
28+
final unit = await RuleTestHelper.resolveFromFile(_correctExamplePath);
29+
final issues = MissingTestAssertionRule().check(unit);
30+
31+
RuleTestHelper.verifyNoIssues(issues);
32+
});
33+
34+
test('with custom config reports no issues', () async {
35+
final unit =
36+
await RuleTestHelper.resolveFromFile(_customAssertionsExamplePath);
37+
final config = {
38+
'include-assertions': [
39+
'verify',
40+
],
41+
};
42+
final issues = MissingTestAssertionRule(config).check(unit);
43+
44+
RuleTestHelper.verifyNoIssues(issues);
45+
});
46+
47+
test('with default config reports about found issues', () async {
48+
final unit = await RuleTestHelper.resolveFromFile(_incorrectExamplePath);
49+
final issues = MissingTestAssertionRule().check(unit);
50+
51+
RuleTestHelper.verifyIssues(
52+
issues: issues,
53+
startLines: [4, 10, 15],
54+
startColumns: [3, 3, 3],
55+
locationTexts: [
56+
"test('bad unit test', () {\n"
57+
' final a = 1;\n'
58+
' final b = 2;\n'
59+
' final c = a + 1;\n'
60+
' })',
61+
"testWidgets('bad widget test', (WidgetTester tester) async {\n"
62+
' await tester.pumpWidget(MyApp());\n'
63+
' await tester.pumpAndSettle();\n'
64+
' })',
65+
'test(null, () => 1 == 1)',
66+
],
67+
messages: [
68+
'Missing test assertion.',
69+
'Missing test assertion.',
70+
'Missing test assertion.',
71+
],
72+
);
73+
});
74+
});
75+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import RuleDetails from '@site/src/components/RuleDetails';
2+
3+
<RuleDetails version="4.21.0" severity="style" />
4+
5+
Warns that there is no assertion in the test.
6+
7+
Use `include-assertions` configuration, if you want to include specific assertions.
8+
9+
### ⚙️ Config example {#config-example}
10+
11+
```yaml
12+
dart_code_metrics:
13+
...
14+
rules:
15+
...
16+
- missing-test-assertion:
17+
include-assertions:
18+
- verify
19+
```
20+
21+
### Example {#example}
22+
23+
**❌ Bad:**
24+
25+
```dart
26+
test('bad unit test', () {
27+
// Given
28+
final a = 1;
29+
final b = 2;
30+
31+
// When
32+
final c = a + 1;
33+
});
34+
```
35+
36+
**✅ Good:**
37+
38+
```dart
39+
test('good unit test', () {
40+
// Given
41+
final a = 1;
42+
final b = 2;
43+
44+
// When
45+
final c = a + 1;
46+
47+
// Then : actual assertion
48+
expect(b, c);
49+
});
50+
```

0 commit comments

Comments
 (0)