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 the {@code expected} parameter on JUnit's {@code @Test} annotation.
13   *
14   * <p>The JUnit 4 construct {@code @Test(expected = SomeException.class)}
15   * is too coarse: the whole test body is inspected for the exception, so a
16   * failure during set-up (e.g. an unexpected {@code NullPointerException})
17   * silently satisfies the assertion. In addition, nothing can be checked
18   * about the message or the cause of the thrown exception. Prefer
19   * {@code Assertions.assertThrows(...)} (JUnit 5) or a Hamcrest-style
20   * {@code try}/{@code catch} with explicit assertions instead. See
21   * <a href="https://github.com/yegor256/qulice/issues/668">#668</a>.</p>
22   *
23   * @since 0.24
24   */
25  public final class ProhibitTestExpectedCheck extends AbstractCheck {
26  
27      @Override
28      public int[] getDefaultTokens() {
29          return this.getRequiredTokens();
30      }
31  
32      @Override
33      public int[] getAcceptableTokens() {
34          return this.getRequiredTokens();
35      }
36  
37      @Override
38      public int[] getRequiredTokens() {
39          return new int[] {TokenTypes.ANNOTATION};
40      }
41  
42      @Override
43      public void visitToken(final DetailAST ast) {
44          if (ProhibitTestExpectedCheck.isTest(ast)
45              && ProhibitTestExpectedCheck.hasExpected(ast)) {
46              this.log(
47                  ast.getLineNo(),
48                  "@Test(expected = ...) is not allowed, use Assertions.assertThrows() instead"
49              );
50          }
51      }
52  
53      /**
54       * Is this a JUnit {@code @Test} annotation (short or fully-qualified)?
55       * @param ast The ANNOTATION node
56       * @return True if its simple name is {@code Test}
57       */
58      private static boolean isTest(final DetailAST ast) {
59          final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
60          final boolean match;
61          if (ident == null) {
62              final DetailAST dot = ast.findFirstToken(TokenTypes.DOT);
63              match = dot != null
64                  && dot.getLastChild() != null
65                  && "Test".equals(dot.getLastChild().getText());
66          } else {
67              match = "Test".equals(ident.getText());
68          }
69          return match;
70      }
71  
72      /**
73       * Does this annotation have an {@code expected} member?
74       * @param ast The ANNOTATION node
75       * @return True if an {@code expected = ...} pair is present
76       */
77      private static boolean hasExpected(final DetailAST ast) {
78          boolean found = false;
79          DetailAST child = ast.getFirstChild();
80          while (child != null) {
81              if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
82                  final DetailAST name = child.findFirstToken(TokenTypes.IDENT);
83                  if (name != null && "expected".equals(name.getText())) {
84                      found = true;
85                      break;
86                  }
87              }
88              child = child.getNextSibling();
89          }
90          return found;
91      }
92  }