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      /**
33       * Default constructor.
34       */
35      public ProhibitTestMethodNameCheck() {
36          // nothing to initialize
37      }
38  
39      @Override
40      public int[] getDefaultTokens() {
41          return this.getRequiredTokens();
42      }
43  
44      @Override
45      public int[] getAcceptableTokens() {
46          return this.getRequiredTokens();
47      }
48  
49      @Override
50      public int[] getRequiredTokens() {
51          return new int[] {TokenTypes.METHOD_DEF};
52      }
53  
54      @Override
55      public void visitToken(final DetailAST ast) {
56          if (ProhibitTestMethodNameCheck.isTest(ast)) {
57              final DetailAST name = ast.findFirstToken(TokenTypes.IDENT);
58              final String text = name.getText();
59              if (ProhibitTestMethodNameCheck.startsWithForbidden(text)) {
60                  this.log(
61                      name.getLineNo(),
62                      String.format(
63                          "Test method name \"%s\" must not start with \"test\" or \"should\", use a verb that describes the behaviour",
64                          text
65                      )
66                  );
67              }
68          }
69      }
70  
71      private static boolean isTest(final DetailAST ast) {
72          final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
73          boolean found = false;
74          if (modifiers != null) {
75              DetailAST child = modifiers.getFirstChild();
76              while (child != null) {
77                  if (child.getType() == TokenTypes.ANNOTATION
78                      && ProhibitTestMethodNameCheck.isTestAnnotation(child)) {
79                      found = true;
80                      break;
81                  }
82                  child = child.getNextSibling();
83              }
84          }
85          return found;
86      }
87  
88      private static boolean isTestAnnotation(final DetailAST ast) {
89          final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
90          final boolean match;
91          if (ident == null) {
92              final DetailAST dot = ast.findFirstToken(TokenTypes.DOT);
93              match = dot != null
94                  && dot.getLastChild() != null
95                  && "Test".equals(dot.getLastChild().getText());
96          } else {
97              match = "Test".equals(ident.getText());
98          }
99          return match;
100     }
101 
102     private static boolean startsWithForbidden(final String name) {
103         return startsWithWord(name, "should")
104             || startsWithWord(name, "test") && !startsWithWord(name, "tests");
105     }
106 
107     private static boolean startsWithWord(final String name, final String prefix) {
108         final boolean result;
109         if (name.startsWith(prefix)) {
110             if (name.length() == prefix.length()) {
111                 result = true;
112             } else {
113                 final char next = name.charAt(prefix.length());
114                 result = Character.isUpperCase(next) || next == '_';
115             }
116         } else {
117             result = false;
118         }
119         return result;
120     }
121 }