diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/AssertStatementInTestCheckSample.java b/java-checks-test-sources/default/src/test/java/checks/tests/AssertStatementInTestCheckSample.java new file mode 100644 index 00000000000..7b1989e45f8 --- /dev/null +++ b/java-checks-test-sources/default/src/test/java/checks/tests/AssertStatementInTestCheckSample.java @@ -0,0 +1,67 @@ +package checks.tests; + +import org.junit.jupiter.api.AfterEach; + +public class AssertStatementInTestCheckSample { + + @org.junit.Test + public void verifiesWithJUnit4() { + assert Account.activeCount() == 1; // Noncompliant {{Replace this "assert" statement with an assertion from the testing framework.}} +// ^^^^^^ + org.junit.Assert.assertEquals(1, Account.activeCount()); + } + + @org.junit.jupiter.api.Test + void verifiesWithJupiter() { + assert Account.activeCount() == 1 : "an account should be active"; // Noncompliant +// ^^^^^^ + org.junit.jupiter.api.Assertions.assertEquals(1, Account.activeCount(), "an account should be active"); + } + + @org.testng.annotations.Test + public void verifiesWithTestNg() { + assert Account.activeCount() == 1; // Noncompliant +// ^^^^^^ + org.testng.Assert.assertEquals(Account.activeCount(), 1); + } + + @org.junit.jupiter.api.Test + void verifiesInControlFlow() { + if (Account.activeCount() > 0) { + assert Account.activeCount() == 1; // Noncompliant +// ^^^^^^ + } + } + + @org.junit.jupiter.api.TestTemplate + void verifiesInLambda() { + Runnable verification = () -> { + assert Account.activeCount() == 1; // Noncompliant +// ^^^^^^ + }; + verification.run(); + } + + @AfterEach + void resetState() { + assert Account.activeCount() == 0; + } + + void helper() { + assert Account.activeCount() >= 0; + } + + @Test + void customTestAnnotationIsIgnored() { + assert Account.activeCount() == 1; + } + + @interface Test { + } + + static class Account { + static int activeCount() { + return 1; + } + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/AssertStatementInTestCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/AssertStatementInTestCheck.java new file mode 100644 index 00000000000..b5d51d22c8a --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/AssertStatementInTestCheck.java @@ -0,0 +1,47 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.tests; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.java.model.ExpressionUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.AssertStatementTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +import static org.sonar.java.utils.UnitTestUtils.hasTestAnnotation; + +@Rule(key = "S9427") +public class AssertStatementInTestCheck extends IssuableSubscriptionVisitor { + + private static final String MESSAGE = "Replace this \"assert\" statement with an assertion from the testing framework."; + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.ASSERT_STATEMENT); + } + + @Override + public void visitNode(Tree tree) { + var assertStatement = (AssertStatementTree) tree; + var enclosingTree = ExpressionUtils.getEnclosingTree(assertStatement, Tree.Kind.METHOD); + if (enclosingTree instanceof MethodTree method && hasTestAnnotation(method)) { + reportIssue(assertStatement.assertKeyword(), MESSAGE); + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/tests/AssertStatementInTestCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/tests/AssertStatementInTestCheckTest.java new file mode 100644 index 00000000000..e9b36be1b9c --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/tests/AssertStatementInTestCheckTest.java @@ -0,0 +1,45 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.tests; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.testCodeSourcesPath; + +class AssertStatementInTestCheckTest { + + private static final String SAMPLE = testCodeSourcesPath("checks/tests/AssertStatementInTestCheckSample.java"); + private static final AssertStatementInTestCheck CHECK = new AssertStatementInTestCheck(); + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(SAMPLE) + .withCheck(CHECK) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(SAMPLE) + .withCheck(CHECK) + .withoutSemantic() + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9427.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9427.html new file mode 100644 index 00000000000..a5a838f87b3 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9427.html @@ -0,0 +1,118 @@ +

This rule raises an issue when a language’s built-in assertion statement is used within a test method, identified by test framework annotations or +markers that designate methods as test cases.

+

Why is this an issue?

+

Native assertion statements in many languages are inappropriate for test code because they can be disabled or controlled by runtime configuration. +Unless explicitly enabled through compiler flags, runtime options, or environment settings, these assertion statements may do nothing—your test will +pass even when the condition fails.

+

This creates a false sense of security. Tests that appear to pass may not actually be verifying anything.

+

Additionally, when native assertions do execute, they provide minimal diagnostic information. A failed assertion typically throws a generic error +with little context about what went wrong. In contrast, testing framework assertion methods provide:

+ +

Testing frameworks are specifically designed for writing tests and handle assertions properly in all execution modes.

+

What is the potential impact?

+

Using assertion mechanisms that can be disabled through runtime configuration in tests can lead to:

+ +

How to fix it in JUnit 4

+

Replace the Java assert statement with JUnit 4’s static assertion methods from the org.junit.Assert class. Use methods +like assertEquals(), assertTrue(), assertFalse(), assertNull(), etc., which provide clear error +messages and always execute.

+

Code examples

+

Noncompliant code example

+
+import org.junit.Test;
+
+public class FooTest {
+    @Test
+    public void testSomething() {
+        assert Foo.bar == 1; // Noncompliant
+    }
+}
+
+

Compliant solution

+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+public class FooTest {
+    @Test
+    public void testSomething() {
+        assertEquals(1, Foo.bar);
+    }
+}
+
+

How to fix it in JUnit 5

+

Replace the Java assert statement with JUnit 5’s static assertion methods from the org.junit.jupiter.api.Assertions +class. JUnit 5 provides improved assertion methods with better messages and optional custom failure messages.

+

Code examples

+

Noncompliant code example

+
+import org.junit.jupiter.api.Test;
+
+public class FooTest {
+    @Test
+    void testSomething() {
+        assert Foo.bar == 1; // Noncompliant
+    }
+}
+
+

Compliant solution

+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FooTest {
+    @Test
+    void testSomething() {
+        assertEquals(1, Foo.bar, "Foo.bar should be 1");
+    }
+}
+
+

How to fix it in TestNG

+

Replace the Java assert statement with TestNG’s static assertion methods from the org.testng.Assert class. TestNG +assertions provide detailed failure messages and integrate with TestNG’s reporting features.

+

Code examples

+

Noncompliant code example

+
+import org.testng.annotations.Test;
+
+public class FooTest {
+    @Test
+    public void testSomething() {
+        assert Foo.bar == 1; // Noncompliant
+    }
+}
+
+

Compliant solution

+
+import org.testng.annotations.Test;
+import static org.testng.Assert.assertEquals;
+
+public class FooTest {
+    @Test
+    public void testSomething() {
+        assertEquals(Foo.bar, 1);
+    }
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9427.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9427.json new file mode 100644 index 00000000000..7e19ae1b94c --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9427.json @@ -0,0 +1,25 @@ +{ + "title": "Java assert statements should not be used in test code", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "junit", + "assertion", + "tests" + ], + "defaultSeverity": "Critical", + "ruleSpecification": "RSPEC-9427", + "sqKey": "S9427", + "scope": "Tests", + "quickfix": "unknown", + "code": { + "impacts": { + "RELIABILITY": "HIGH" + }, + "attribute": "COMPLETE" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9427 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9427 new file mode 100644 index 00000000000..e69de29bb2d