Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"ruleKey": "S3577",
"hasTruePositives": true,
"falseNegatives": 46,
"falseNegatives": 47,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S8692",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
7 changes: 7 additions & 0 deletions its/ruling/src/test/resources/eclipse-jetty/java-S8692.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java": [
80,
87,
91
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package checks.tests;

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

class SystemClockCheckSample {

@Mock
private Clock clock;

static class SecurityService {
private final Clock clock;

public SecurityService(Clock clock) {
this.clock = clock;
}

public boolean isTokenValid(Instant issuedAt) {
return issuedAt.isAfter(Instant.now(clock).minus(1, ChronoUnit.HOURS));
}
}

@Test
void testSystemClockInstants() {
Instant now = Instant.now(); // Noncompliant {{Do not use the system clock in tests.}}
// ^^^^^^^^^^^^^
Instant now2 = Instant.now(Clock.system(ZoneId.systemDefault())); // Noncompliant {{Do not use the system clock in tests.}}
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
assertTrue(now.isBefore(now2));
}

void testLocalDateTimeTypes() {
LocalDateTime dateTime1 = LocalDateTime.now(Clock.systemUTC()); // Noncompliant {{Do not use the system clock in tests.}}
// ^^^^^^^^^^^^^^^^^
LocalDateTime dateTime2 = LocalDateTime.now(); // Noncompliant {{Do not use the system clock in tests.}}
// ^^^^^^^^^^^^^^^^^^^
assertTrue(dateTime1.isBefore(dateTime2));
}

@Test
void testInjectSystemClock() {
SecurityService securityService = new SecurityService(Clock.systemDefaultZone()); // Noncompliant {{Do not use the system clock in tests.}}
// ^^^^^^^^^^^^^^^^^^^^^^^^^
assertTrue(securityService.isTokenValid(Instant.now(Clock.system(ZoneId.systemDefault())).minusSeconds(60))); // Noncompliant {{Do not use the system clock in tests.}}
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

@Test
void testFixedClock() {
Instant start = Instant.now(Clock.fixed(Instant.parse("2026-05-07T10:00:00Z"), ZoneOffset.UTC)); // Compliant
Instant later = start.plus(1, ChronoUnit.MINUTES);
assertTrue(start.isBefore(later));
}

@Test
void testInjectFixedClock() {
Instant fixedPoint = Instant.parse("2026-05-07T10:00:00Z"); // Compliant
when(clock.instant()).thenReturn(fixedPoint);
when(clock.getZone()).thenReturn(ZoneOffset.UTC);

SecurityService service = new SecurityService(clock);
Instant issuedAt = Instant.parse("2026-05-07T09:30:00Z"); // Compliant
assertTrue(service.isTokenValid(issuedAt));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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.sonar.check.Rule;
import org.sonar.java.checks.methods.AbstractMethodDetection;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;

@Rule(key = "S8692")
public class SystemClockCheck extends AbstractMethodDetection {

private static final MethodMatchers SYSTEM_CLOCK_MATCHERS = MethodMatchers.or(
MethodMatchers.create()
.ofTypes("java.time.LocalDate",
"java.time.LocalTime",
"java.time.LocalDateTime",
"java.time.MonthDay",
"java.time.Year",
"java.time.YearMonth",
"java.time.ZonedDateTime",
"java.time.OffsetDateTime",
"java.time.OffsetTime",
"java.time.Instant")
.names("now")
.addWithoutParametersMatcher()
.addParametersMatcher("java.time.ZoneId")
.build(),
MethodMatchers.create()
.ofTypes("java.time.Clock")
.names("systemUTC", "systemDefaultZone", "system")
.withAnyParameters()
.build()
);

@Override
protected MethodMatchers getMethodInvocationMatchers() {
return SYSTEM_CLOCK_MATCHERS;
}

@Override
protected void onMethodInvocationFound(MethodInvocationTree mit) {
reportIssue(mit, "Do not use the system clock in tests.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 SystemClockCheckTest {

@Test
void test() {
CheckVerifier.newVerifier()
.onFile(testCodeSourcesPath("checks/tests/SystemClockCheckSample.java"))
.withCheck(new SystemClockCheck())
.verifyIssues();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<p>Reading the system clock directly within unit tests introduces non-determinism. Tests that rely on the current wall-clock time are "flaky": they
may pass today but fail tomorrow, or behave differently depending on the time zone and local time of the CI/CD runner.</p>
<p>To ensure that they are repeatable and predictable, <code>.now()</code> methods like <code>LocalDate.now()</code>,
<code>LocalDateTime.now()</code>, or <code>Instant.now()</code> should only be called with a fixed clock in unit tests. <code>Clock.system(ZoneId
zone)</code>, <code>Clock.systemDefaultZone()</code> and <code>Clock.systemUTC()</code> should also be avoided in tests.</p>
<h2>Why is this an issue?</h2>
<p>When a test uses the system clock, it relies on an external, uncontrollable state. This makes it impossible to:</p>
<ul>
<li>Test edge cases (like leap years or daylight saving time transitions) consistently.</li>
<li>Verify logic that depends on a specific point in time without manually changing the environment clock.</li>
<li>Guarantee that a test suite will yield the same results across different execution environments.</li>
</ul>
<h2>How to fix it</h2>
<p>The best practice is to inject a <code>java.time.Clock</code> instance into the class or method being tested. In production code,
<code>Clock.systemDefaultZone()</code> can be used, whereas in test code, <code>Clock.fixed(Instant fixedInstant, ZoneId zone)</code> should be
called, or a mock <code>Clock</code> be injected. This ensures that tests remain deterministic regardless of when or where they are run.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<p>The test below is flaky because assumes that the second call to <code>Instant.now()</code> returns an instant that is later than the first, which
might not be true depending on the environment in which the test is run.</p>
<pre data-diff-id="1" data-diff-type="noncompliant">
@Test
void testTimeDifference() {
Instant instant1 = Instant.now();
Instant instant2 = Instant.now();
assertTrue(instant1.isBefore(instant2)); // Noncompliant
}
</pre>
<p>In the second example below, the service uses a static call to <code>Instant.now()</code>. This makes it impossible to mock the time without using
advanced (and often discouraged) static mocking techniques.</p>
<pre data-diff-id="2" data-diff-type="noncompliant">
public class SecurityService {
public boolean isTokenValid(Instant issuedAt) {
return issuedAt.isAfter(Instant.now().minus(1, ChronoUnit.HOURS));
}
}

public class SecurityServiceTest {
@Test
void testTokenValidation() {
SecurityService service = new SecurityService();
assertTrue(service.isTokenValid(Instant.now().minusSeconds(60))); // Noncompliant: the test is non-deterministic and depends on the execution time.
}
}
</pre>
<h4>Compliant solution</h4>
<p>Here, the first instant is guaranteed to be before the second, because it is fixed, and the second is explicitly set to a later moment.</p>
<pre data-diff-id="1" data-diff-type="compliant">
@Test
void testTimeDifference() {
Instant instant1 = Instant.now(Clock.fixed(Instant.parse("2026-05-07T10:00:00Z"), ZoneOffset.UTC)); // Compliant
Instant instant2 = instant1.plus(1, ChronoUnit.MINUTES);
assertTrue(instant1.isBefore(instant2));
}
</pre>
<p>In the solution below, the Clock is a field in the class (often injected by a framework like Spring). In the test, we use @Mock to control exactly
what time the service "thinks" it is.</p>
<pre data-diff-id="2" data-diff-type="compliant">
public class SecurityService {
private final Clock clock;

public SecurityService(Clock clock) {
this.clock = clock;
}

public boolean isTokenValid(Instant issuedAt) {
return issuedAt.isAfter(Instant.now(clock).minus(1, ChronoUnit.HOURS));
}
}

@ExtendWith(MockitoExtension.class)
public class SecurityServiceTest {
@Mock
private Clock clock;

@Test
void testTokenValidationWithMock() {
Instant fixedPoint = Instant.parse("2026-05-07T10:00:00Z");

// Stub the mock clock to behave like a fixed clock.
when(clock.instant()).thenReturn(fixedPoint);
when(clock.getZone()).thenReturn(ZoneOffset.UTC);

SecurityService service = new SecurityService(clock);

Instant issuedAt = Instant.parse("2026-05-07T09:30:00Z");
assertTrue(service.isTokenValid(issuedAt)); // Compliant: this is now deterministic, as 09:30 is within 1 hour of 10:00.
}
}
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li><a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/Clock.html">Official documentation for the Clock class</a></li>
</ul>
<h3>Articles &amp; blog posts</h3>
<ul>
<li><a href="https://berrueta.net/software/2025/06/14/testing-and-time.html">Testing and Time in Java</a></li>
<li><a href="https://www.baeldung.com/java-override-system-time">Overriding System Time for Testing in Java</a></li>
</ul>

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"title": "The system clock should not be used in unit tests",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "1h"
},
"tags": [
"java8",
"datetime",
"tests"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-8692",
"sqKey": "S8692",
"scope": "Tests",
"quickfix": "unknown",
"code": {
"impacts": {
"MAINTAINABILITY": "HIGH",
"RELIABILITY": "MEDIUM"
},
"attribute": "CONVENTIONAL"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@
"S8465",
"S8469",
"S8491",
"S8688"
"S8688",
"S8692"
]
}
Loading