View Javadoc
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 }