1 /*
2 * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3 * SPDX-License-Identifier: MIT
4 */
5 package com.qulice.checkstyle;
6
7 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8 import com.puppycrawl.tools.checkstyle.api.DetailAST;
9 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10
11 /**
12 * Forbids {@code @Test}-annotated methods whose name starts with
13 * {@code test} or {@code should}.
14 *
15 * <p>Test method names should start with a verb describing the scenario
16 * under test, not with the generic prefixes {@code test} (which merely
17 * repeats the annotation) or {@code should} (which frames the test as a
18 * specification rather than as a behaviour). A method called
19 * {@code parsesIntegers()} is more informative than
20 * {@code testParseInteger()} or {@code shouldParseInteger()}. Names
21 * starting with {@code tests} are allowed, since the method may be
22 * responsible for testing something (e.g. {@code testsAllBranches()}).
23 * See
24 * <a href="https://www.yegor256.com/2014/04/27/typical-mistakes-in-java-code.html#test-method-names">
25 * this article</a> and
26 * <a href="https://github.com/yegor256/qulice/issues/663">#663</a>.</p>
27 *
28 * @since 0.24
29 */
30 public final class ProhibitTestMethodNameCheck extends AbstractCheck {
31
32 @Override
33 public int[] getDefaultTokens() {
34 return this.getRequiredTokens();
35 }
36
37 @Override
38 public int[] getAcceptableTokens() {
39 return this.getRequiredTokens();
40 }
41
42 @Override
43 public int[] getRequiredTokens() {
44 return new int[] {TokenTypes.METHOD_DEF};
45 }
46
47 @Override
48 public void visitToken(final DetailAST ast) {
49 if (ProhibitTestMethodNameCheck.isTest(ast)) {
50 final DetailAST name = ast.findFirstToken(TokenTypes.IDENT);
51 final String text = name.getText();
52 if (ProhibitTestMethodNameCheck.startsWithForbidden(text)) {
53 this.log(
54 name.getLineNo(),
55 String.format(
56 "Test method name \"%s\" must not start with \"test\" or \"should\", use a verb that describes the behaviour",
57 text
58 )
59 );
60 }
61 }
62 }
63
64 /**
65 * Does this method declaration carry a JUnit {@code @Test} annotation?
66 * @param ast The METHOD_DEF node
67 * @return True if annotated with {@code @Test}
68 */
69 private static boolean isTest(final DetailAST ast) {
70 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
71 boolean found = false;
72 if (modifiers != null) {
73 DetailAST child = modifiers.getFirstChild();
74 while (child != null) {
75 if (child.getType() == TokenTypes.ANNOTATION
76 && ProhibitTestMethodNameCheck.isTestAnnotation(child)) {
77 found = true;
78 break;
79 }
80 child = child.getNextSibling();
81 }
82 }
83 return found;
84 }
85
86 /**
87 * Is this ANNOTATION node the JUnit {@code @Test} annotation
88 * (either the short or fully-qualified form)?
89 * @param ast The ANNOTATION node
90 * @return True if its simple name is {@code Test}
91 */
92 private static boolean isTestAnnotation(final DetailAST ast) {
93 final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
94 final boolean match;
95 if (ident == null) {
96 final DetailAST dot = ast.findFirstToken(TokenTypes.DOT);
97 match = dot != null
98 && dot.getLastChild() != null
99 && "Test".equals(dot.getLastChild().getText());
100 } else {
101 match = "Test".equals(ident.getText());
102 }
103 return match;
104 }
105
106 /**
107 * Does the name begin with a forbidden prefix?
108 * @param name The method name
109 * @return True if it starts with {@code should} or with {@code test}
110 * but not with {@code tests}
111 */
112 private static boolean startsWithForbidden(final String name) {
113 return startsWithWord(name, "should")
114 || startsWithWord(name, "test") && !startsWithWord(name, "tests");
115 }
116
117 /**
118 * Does {@code name} start with {@code prefix} as a whole lowercase
119 * word (either equal, or followed by an uppercase/underscore boundary)?
120 * @param name The method name
121 * @param prefix The prefix to check
122 * @return True if the prefix is a word at the start of the name
123 */
124 private static boolean startsWithWord(final String name, final String prefix) {
125 final boolean result;
126 if (name.startsWith(prefix)) {
127 if (name.length() == prefix.length()) {
128 result = true;
129 } else {
130 final char next = name.charAt(prefix.length());
131 result = Character.isUpperCase(next) || next == '_';
132 }
133 } else {
134 result = false;
135 }
136 return result;
137 }
138 }