Skip to content

Commit d8465e5

Browse files
committed
java: add stub time
1 parent f9853b3 commit d8465e5

File tree

5 files changed

+132
-3
lines changed

5 files changed

+132
-3
lines changed

java/data-structures/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
<artifactId>data-structures</artifactId>
1313

1414
<properties>
15-
<assertj.version>3.26.0</assertj.version>
16-
<junit.version>5.10.2</junit.version>
15+
<assertj.version>3.27.3</assertj.version>
16+
<junit.version>5.11.4</junit.version>
1717
</properties>
1818

1919
<dependencyManagement>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package lin.louis.data_structures.time;
2+
3+
import java.time.Clock;
4+
import java.time.Instant;
5+
import java.time.LocalDate;
6+
import java.time.LocalDateTime;
7+
import java.time.ZoneOffset;
8+
9+
/**
10+
* The Java API, such as {@link LocalDateTime#now()}, {@link Instant#now()}, or {@link Clock#systemUTC()}, can be
11+
* difficult to stub because they are closely coupled with one another. Stubbing one can be quite challenging.
12+
* While we can use <a href="https://www.baeldung.com/mockito-mock-static-methods">Mockito static methods</a>,
13+
* some libraries use one of the three above (or any variant with some parameters), which might generate
14+
* unexpected behavior in your tests or, even worse, cause flaky tests.
15+
* <br/>
16+
* One option is to create a helper that your code MUST use to get the current date and time instead of using
17+
* the Java ones. This way, it's easier to stub and does not impact the behavior of the libraries.
18+
* <br/>
19+
* However, the drawback is that you need to use this specific helper in your code, and if you expect some
20+
* specific behavior from the libraries based on the current time, this option won't work.
21+
*/
22+
public class TimeHelper {
23+
24+
/**
25+
* No setter is exposed to ensure that only tests using {@link TimeTraveler} can set the value of the thread-local
26+
* variable via reflection. This prevents the implementation code from accidentally modifying the current time.
27+
*/
28+
private static final ThreadLocal<LocalDateTime> NOW = new ThreadLocal<>();
29+
30+
public static LocalDateTime nowUTC() {
31+
var now = NOW.get();
32+
return now != null ? now : LocalDateTime.now(ZoneOffset.UTC);
33+
}
34+
35+
public static LocalDate todayUTC() {
36+
return nowUTC().toLocalDate();
37+
}
38+
39+
private TimeHelper() {}
40+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package lin.louis.data_structures.time;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.time.LocalDateTime;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
10+
class TimeHelperTest {
11+
private static final LocalDateTime NOW = LocalDateTime.parse("2025-02-09T18:30:31");
12+
13+
@RegisterExtension
14+
private static TimeTraveler TIME_TRAVELER = TimeTraveler.travelTo(NOW);
15+
16+
@Test
17+
void currentTimeHasBeenStubbed() {
18+
var actual = TimeHelper.nowUTC();
19+
20+
assertThat(actual).isEqualTo(NOW);
21+
}
22+
23+
@Test
24+
void currentTimeCanBeInjected(LocalDateTime now) {
25+
assertThat(now).isEqualTo(NOW);
26+
}
27+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package lin.louis.data_structures.time;
2+
3+
import java.time.LocalDateTime;
4+
5+
import org.junit.jupiter.api.extension.AfterAllCallback;
6+
import org.junit.jupiter.api.extension.BeforeAllCallback;
7+
import org.junit.jupiter.api.extension.ExtensionContext;
8+
import org.junit.jupiter.api.extension.ParameterContext;
9+
import org.junit.jupiter.api.extension.ParameterResolutionException;
10+
import org.junit.jupiter.api.extension.ParameterResolver;
11+
12+
public class TimeTraveler implements BeforeAllCallback, AfterAllCallback, ParameterResolver {
13+
14+
private final LocalDateTime now;
15+
16+
/**
17+
* Represents the level for which the tests are located. We need to track it
18+
* because we can have multiple nested tests.
19+
*/
20+
private int testLevel;
21+
22+
public static TimeTraveler travelTo(LocalDateTime dateTime) {
23+
return new TimeTraveler(dateTime);
24+
}
25+
26+
private TimeTraveler(LocalDateTime now) {
27+
this.now = now;
28+
}
29+
30+
@Override
31+
public void beforeAll(ExtensionContext context) throws Exception {
32+
timeKeeperThreadLocal().set(now);
33+
testLevel++;
34+
}
35+
36+
@Override
37+
public void afterAll(ExtensionContext context) throws Exception {
38+
testLevel--;
39+
if (testLevel == 0) {
40+
timeKeeperThreadLocal().remove();
41+
}
42+
}
43+
44+
@Override
45+
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
46+
throws ParameterResolutionException {
47+
return parameterContext.getParameter().getType() == LocalDateTime.class;
48+
}
49+
50+
@Override
51+
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
52+
throws ParameterResolutionException {
53+
return now;
54+
}
55+
56+
@SuppressWarnings("unchecked")
57+
private ThreadLocal<LocalDateTime> timeKeeperThreadLocal() throws NoSuchFieldException, IllegalAccessException {
58+
var nowField = TimeHelper.class.getDeclaredField("NOW");
59+
nowField.setAccessible(true);
60+
return (ThreadLocal<LocalDateTime>) nowField.get(null);
61+
}
62+
}

java/security/src/main/java/lin/louis/security/sign/X509Certificates.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
public class X509Certificates {
44
public static final String PRIVATE_KEY = "MIICXQIBAAKBgQDWCOCt8f72/PE4dfEhaDZY6bjue3MFu7LbSOr3dTGHrPXlgAA6j+ATct6Rb8eBcxvPhqKGwv0Ho+dygT78K6xZmze7n27aY3Dln2MS9f9uS7AoElQRQxgqmuXPS1VRi3YBOdqk4z2D/+cH0/FACUFpGc5fuECIWFiugK/IhgWkGQIDAQABAoGANG1GQ7VUI8G/gHn7T5iMP2k4oEni2dOpMueAjo7JTBeEv+uDotSdKYZomC1OLBo7BLFQ3Duk6Rsv1S9tcy1rcLDIhyxLjkX7SbU7pRQmsiTMKbxoyP0Fck0TORp9M6I+u22LrokJ10/KMcV5dg9e5wRiqSvKTVaTzqA33VAke1ECQQDw8Jtc1M5nCDRP5k+gCwTnuXYV9yuAw+nNy/hinRMdJafXcjy0hfDG6jKKSIv6ulobKlrbQ8YIC+4WAQtok9YFAkEA42m/ftrAWxw7GgrxbSVA0CDiCpyBIrDil8f606H2tZeG0EAPL45o668AgoXdvs7pDUDh6nBwE3WCZdOp9lx+BQJBAMPu9nj8eckhy+C560C8FUYX9OaR9Semqkh4Ocp/795BFAfJV4J6db5dD7KSonrH9qSmwfITYESE5x2vxcZKir0CQQCcqO2VWuamHSWNxDoaoU4r0mtFOhkvp8EBJG9jOTD2WcMyVN7hOO6IZY8pW0StvGYJjkfTM8/RZ+MDeLOeFottAkAxMWFGB+HwF5NZdNzwq9Q4DLu/geI/H9HBejAigHnNUfTInFWxlJOoGQuHwTmjf1G48NicXya8IsLsPgBvHs8w";
5-
public static final String PUBLIC_KEY = "MIICdDCCAd2gAwIBAgIJAIjyT+oKZ7UKMA0GCSqGSIb3DQEBBQUAMFMxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAMBgNVBAcMBVBhcmlzMREwDwYDVQQKDAhGb29iYXJDQTETMBEGA1UEAwwKKi5sb2NhbC5mcjAeFw0xNDA4MTQxNjE3MjlaFw0yNDA4MTExNjE3MjlaMFMxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAMBgNVBAcMBVBhcmlzMREwDwYDVQQKDAhGb29iYXJDQTETMBEGA1UEAwwKKi5sb2NhbC5mcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1gjgrfH+9vzxOHXxIWg2WOm47ntzBbuy20jq93Uxh6z15YAAOo/gE3LekW/HgXMbz4aihsL9B6PncoE+/CusWZs3u59u2mNw5Z9jEvX/bkuwKBJUEUMYKprlz0tVUYt2ATnapOM9g//nB9PxQAlBaRnOX7hAiFhYroCvyIYFpBkCAwEAAaNQME4wHQYDVR0OBBYEFDttawgSlrgwwc92/asJh/JfL2bzMB8GA1UdIwQYMBaAFDttawgSlrgwwc92/asJh/JfL2bzMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEArrGXPUJ11XnjsRAjG3L8hQS/ncnK1wFD6uVJTIntt8jHSCe7VyRkdYtskBgvHmaW7v6YOmaZ5KpN4Ii7cH+Lf4OEXO76IDFBWfknplp54+io+qbgGDTjZoQyiw25QbQgJQbDeKJUpQ8d0cdO05XXgv9eIy6lvD9iHc+UxJOtktA=";
5+
public static final String PUBLIC_KEY = "MIICdDCCAd2gAwIBAgIJAIjyT+oKZ7UKMA0GCSqGSIb3DQEBBQUAMFMxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAMBgNVBAcMBVBhcmlzMREwDwYDVQQKDAhGb29iYXJDQTETMBEGA1UEAwwKKi5sb2NhbC5mcjAeFw0xNDA4MTQxNjE3MjlaFw0yNDA4MTExNjE3MjlaMFMxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAMBgNVBAcMBVBhcmlzMREwDwYDVQQKDAhGb29iYXJDQTETMBEGA1UEAwwKKi5sb2NhbC5mcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1gjgrfH+9vzxOHXxIWg2WOm47ntzBbuy20jq93Uxh6z15YAAOo/gE3LekW/HgXMbz4aihsL9B6PncoE+/CusWZs3u59u2mNw5Z9jEvX/bkuwKBJUEUMYKprlz0tVUYt2ATnapOM9g//nB9PxQAlBaRnOX7hAiFhYroCvyIYFpBkCAwEAAaNQME4wHQYDVR0OBBYEFDttawgSlrgwwc92/asJh/JfL2bzMB8GA1UdIwQYMBaAFDttawgSlrgwwc92/asJh/JfL2bzMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEArrGXPUJ11XnjsRAjG3L8hQS/ncnK1wFD6uVJTIntt8jHSCe7VyRkdYtskBgvHmaW7v6YOmaZ5KpN4Ii7cH+Lf4OEXO76IDFBWfknplp54+io+qbgGDTjZoQyiw25QbQgJQbDeKJUpQ8d0cdO05XXgv9eIy6lvD9iHc+UxJOtktA=";
66
}

0 commit comments

Comments
 (0)