+ * Unlike {@link MockWebServer#takeRequest()}, does not block. Instead, waits
+ * a brief amount of time (10 milliseconds) for the next request. And unlike
+ * {@link MockWebServer#takeRequest(long, TimeUnit)}, converts a checked
+ * InterruptedException to an {@link UncheckedInterruptedException} so that
+ * tests don't need to constantly declare "throws" clauses.
+ *
+ * @param mockWebServer the {@link MockWebServer} which may contain the recorded request
+ * @return the next available {@link RecordedRequest}, or null if not available
+ * @throws UncheckedInterruptedException if the call to get the next request throws InterruptedException
+ */
+ public static RecordedRequest takeRequestOrNull(MockWebServer mockWebServer) {
+ try {
+ return mockWebServer.takeRequest(10, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ LOG.info("Interrupted waiting to get next request", e);
+ Thread.currentThread().interrupt();
+ throw new UncheckedInterruptedException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/kiwiproject/test/okhttp3/mockwebserver/RecordedRequestsTest.java b/src/test/java/org/kiwiproject/test/okhttp3/mockwebserver/RecordedRequestsTest.java
new file mode 100644
index 0000000..6ecb325
--- /dev/null
+++ b/src/test/java/org/kiwiproject/test/okhttp3/mockwebserver/RecordedRequestsTest.java
@@ -0,0 +1,197 @@
+package org.kiwiproject.test.okhttp3.mockwebserver;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.kiwiproject.test.assertj.KiwiAssertJ.assertPresentAndGet;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.awaitility.Durations;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.kiwiproject.base.UncheckedInterruptedException;
+import org.kiwiproject.io.KiwiIO;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@DisplayName("RecordedRequests")
+class RecordedRequestsTest {
+
+ private MockWebServer server;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ server = new MockWebServer();
+ server.start();
+ }
+
+ @AfterEach
+ void tearDown() {
+ KiwiIO.closeQuietly(server);
+ }
+
+ @Nested
+ class TakeRequiredRequest {
+
+ @Test
+ void shouldReturnTheAvailableRequest() {
+ server.enqueue(new MockResponse().setResponseCode(200));
+
+ var path = randomPath();
+ makeRequest(path);
+
+ var recordedRequest = RecordedRequests.takeRequiredRequest(server);
+
+ assertRequest(recordedRequest, path);
+ }
+
+ @Test
+ void shouldThrowIllegalState_WhenNoRequestIsAvailable() {
+ assertThatIllegalStateException()
+ .isThrownBy(() -> RecordedRequests.takeRequiredRequest(server))
+ .withMessage("no request is currently available");
+ }
+ }
+
+ @Nested
+ class TakeRequestOrEmpty {
+
+ @Test
+ void shouldReturnTheAvailableRequest() {
+ server.enqueue(new MockResponse().setResponseCode(202));
+
+ var path = randomPath();
+ makeRequest(path);
+
+ var recordedRequestOpt = RecordedRequests.takeRequestOrEmpty(server);
+
+ var recordedRequest = assertPresentAndGet(recordedRequestOpt);
+ assertRequest(recordedRequest, path);
+ }
+
+ @Test
+ void shouldReturnEmptyOptional_WhenNoRequestIsAvailable() {
+ assertThat(RecordedRequests.takeRequestOrEmpty(server)).isEmpty();
+ }
+ }
+
+ @Nested
+ class AssertNoMoreRequests {
+
+ @Test
+ void shouldPass_WhenThereIsNoRequestAvailable() {
+ assertThatCode(() -> RecordedRequests.assertNoMoreRequests(server))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void shouldFail_WhenAnyRequestIsAvailable() {
+ server.enqueue(new MockResponse().setResponseCode(202));
+
+ var path = randomPath();
+ makeRequest(path);
+
+ assertThatThrownBy(() -> RecordedRequests.assertNoMoreRequests(server))
+ .isNotNull()
+ .hasMessageContaining(
+ "There should not be any more requests, but (at least) one was found");
+ }
+ }
+
+ @Nested
+ class TakeRequestOrNull {
+
+ @Test
+ void shouldReturnTheAvailableRequest() {
+ server.enqueue(new MockResponse().setResponseCode(204));
+
+ var path = randomPath();
+ makeRequest(path);
+
+ var recordedRequestOpt = RecordedRequests.takeRequestOrEmpty(server);
+
+ var recordedRequest = assertPresentAndGet(recordedRequestOpt);
+ assertRequest(recordedRequest, path);
+ }
+
+ @Test
+ void shouldReturnNull_WhenNoRequestIsAvailable() {
+ assertThat(RecordedRequests.takeRequestOrNull(server)).isNull();
+ }
+
+ @Test
+ void shouldThrowUncheckedInterruptedException_IfInterruptedExceptionIsThrown() throws InterruptedException {
+ var mockMockWebServer = mock(MockWebServer.class);
+ when(mockMockWebServer.takeRequest(anyLong(), any(TimeUnit.class)))
+ .thenThrow(new InterruptedException("I interrupt you!"));
+
+ // Execute in separate thread so that the "Thread.currentThread().interrupt()" call
+ // does not interrupt the test thread.
+ var executor = Executors.newSingleThreadExecutor();
+ try {
+ Callable