View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.pmd.rules;
6   
7   import java.util.Set;
8   import net.sourceforge.pmd.lang.java.ast.ASTBlock;
9   import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
10  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
11  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
12  import net.sourceforge.pmd.lang.java.rule.internal.TestFrameworksUtil;
13  
14  /**
15   * Rule to check that JUnit/TestNG test methods do not contain more than
16   * one assertion. Unlike the PMD built-in
17   * {@code UnitTestContainsTooManyAsserts} rule, this implementation only
18   * counts calls whose simple name is one of the known JUnit/Hamcrest
19   * assertion methods, instead of counting any identifier with an
20   * {@code assert}, {@code check} or {@code verify} prefix. The PMD
21   * default produces false positives for unrelated APIs such as
22   * {@code pull.checks()} (jcabi-github) or {@code Mockito.verify(...)},
23   * and the {@code assertThrows} method is also excluded because a common
24   * idiom wraps {@code assertThrows(...).getMessage()} inside an
25   * {@code assertThat} to verify the thrown exception's message in a
26   * single logical check.
27   *
28   * @since 0.26.0
29   */
30  public final class UnitTestContainsTooManyAssertsRule
31      extends AbstractJavaRulechainRule {
32  
33      /**
34       * Method names recognised as JUnit / Hamcrest assertions. The list
35       * intentionally excludes {@code assertThrows}, which is treated as
36       * an exception-capturing helper, not an assertion.
37       */
38      private static final Set<String> ASSERTIONS = Set.of(
39          "assertThat",
40          "assertEquals",
41          "assertNotEquals",
42          "assertTrue",
43          "assertFalse",
44          "assertNull",
45          "assertNotNull",
46          "assertSame",
47          "assertNotSame",
48          "assertArrayEquals",
49          "assertIterableEquals",
50          "assertLinesMatch",
51          "assertDoesNotThrow",
52          "assertTimeout",
53          "assertTimeoutPreemptively",
54          "assertAll",
55          "fail"
56      );
57  
58      /**
59       * Default constructor.
60       */
61      public UnitTestContainsTooManyAssertsRule() {
62          super(ASTMethodDeclaration.class);
63      }
64  
65      @Override
66      public Object visit(final ASTMethodDeclaration method, final Object data) {
67          final ASTBlock body = method.getBody();
68          if (body != null
69              && TestFrameworksUtil.isTestMethod(method)
70              && body.descendants(ASTMethodCall.class)
71                  .filter(UnitTestContainsTooManyAssertsRule::isCountedAssert)
72                  .count() > 1) {
73              this.asCtx(data).addViolation(method);
74          }
75          return data;
76      }
77  
78      private static boolean isCountedAssert(final ASTMethodCall call) {
79          return UnitTestContainsTooManyAssertsRule.ASSERTIONS.contains(
80              call.getMethodName()
81          );
82      }
83  }