diff --git a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java index 1f409e2159..06de5d3ae7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ package org.springframework.test.web.client; import java.io.IOException; import java.net.URI; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -144,25 +146,42 @@ public abstract class AbstractRequestExpectationManager implements RequestExpect @Override public void verify() { - if (this.expectations.isEmpty()) { - return; - } - int count = 0; - for (RequestExpectation expectation : this.expectations) { - if (!expectation.isSatisfied()) { - count++; - } - } + int count = verifyInternal(); if (count > 0) { String message = "Further request(s) expected leaving " + count + " unsatisfied expectation(s).\n"; throw new AssertionError(message + getRequestDetails()); } + } + + @Override + public void verify(Duration timeout) { + Instant endTime = Instant.now().plus(timeout); + do { + if (verifyInternal() == 0) { + return; + } + } + while (Instant.now().isBefore(endTime)); + verify(); + } + + private int verifyInternal() { + if (this.expectations.isEmpty()) { + return 0; + } if (!this.requestFailures.isEmpty()) { throw new AssertionError("Some requests did not execute successfully.\n" + this.requestFailures.entrySet().stream() .map(entry -> "Failed request:\n" + entry.getKey() + "\n" + entry.getValue()) .collect(Collectors.joining("\n", "\n", ""))); } + int count = 0; + for (RequestExpectation expectation : this.expectations) { + if (!expectation.isSatisfied()) { + count++; + } + } + return count; } /** diff --git a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java index 2044297a68..d3c0cd7b41 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.test.web.client; import java.io.IOException; import java.net.URI; +import java.time.Duration; import org.springframework.http.HttpMethod; import org.springframework.http.client.BufferingClientHttpRequestFactory; @@ -110,12 +111,25 @@ public final class MockRestServiceServer { /** * Verify that all expected requests set up via * {@link #expect(RequestMatcher)} were indeed performed. - * @throws AssertionError when some expectations were not met + * @throws AssertionError if not all expectations are met */ public void verify() { this.expectationManager.verify(); } + /** + * Variant of {@link #verify()} that waits for up to the specified time for + * all expectations to be fulfilled. This can be useful for tests that + * involve asynchronous requests. + * @param timeout how long to wait for all expecations to be met + * @throws AssertionError if not all expectations are met by the specified + * timeout, or if any expectation fails at any time before that. + * @since 5.3.4 + */ + public void verify(Duration timeout) { + this.expectationManager.verify(timeout); + } + /** * Reset the internal state removing all expectations and recorded requests. */ diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java index 3a68bf23ad..734a607390 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.test.web.client; import java.io.IOException; +import java.time.Duration; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; @@ -52,11 +53,22 @@ public interface RequestExpectationManager { /** * Verify that all expectations have been met. *

This is a delegate for {@link MockRestServiceServer#verify()}. - * @throws AssertionError when some expectations were not met + * @throws AssertionError if not all expectations are met * @see MockRestServiceServer#verify() */ void verify(); + /** + * Variant of {@link #verify()} that waits for up to the specified time for + * all expectations to be fulfilled. This can be useful for tests that + * involve asynchronous requests. + * @param timeout how long to wait for all expecations to be met + * @throws AssertionError if not all expectations are met by the specified + * timeout, or if any expectation fails at any time before that. + * @since 5.3.4 + */ + void verify(Duration timeout); + /** * Reset the internal state removing all expectations and recorded requests. *

This is a delegate for {@link MockRestServiceServer#reset()}. @@ -75,5 +87,4 @@ public interface RequestExpectationManager { * @throws IOException in case of any validation errors */ ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException; - } diff --git a/spring-test/src/test/java/org/springframework/test/web/client/MockRestServiceServerTests.java b/spring-test/src/test/java/org/springframework/test/web/client/MockRestServiceServerTests.java index 14f62fee21..a9974bddad 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/MockRestServiceServerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/MockRestServiceServerTests.java @@ -17,6 +17,7 @@ package org.springframework.test.web.client; import java.net.SocketException; +import java.time.Duration; import org.junit.jupiter.api.Test; @@ -24,6 +25,7 @@ import org.springframework.test.web.client.MockRestServiceServer.MockRestService import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; import static org.springframework.http.HttpMethod.POST; import static org.springframework.test.web.client.ExpectedCount.once; @@ -171,4 +173,27 @@ public class MockRestServiceServerTests { .withMessageStartingWith("Some requests did not execute successfully"); } + @Test + public void verifyWithTimeout() { + MockRestServiceServerBuilder builder = MockRestServiceServer.bindTo(this.restTemplate); + + MockRestServiceServer server1 = builder.build(); + server1.expect(requestTo("/foo")).andRespond(withSuccess()); + server1.expect(requestTo("/bar")).andRespond(withSuccess()); + this.restTemplate.getForObject("/foo", Void.class); + + assertThatThrownBy(() -> server1.verify(Duration.ofMillis(100))).hasMessage( + "Further request(s) expected leaving 1 unsatisfied expectation(s).\n" + + "1 request(s) executed:\n" + + "GET /foo, headers: [Accept:\"application/json, application/*+json\"]\n"); + + MockRestServiceServer server2 = builder.build(); + server2.expect(requestTo("/foo")).andRespond(withSuccess()); + server2.expect(requestTo("/bar")).andRespond(withSuccess()); + this.restTemplate.getForObject("/foo", Void.class); + this.restTemplate.getForObject("/bar", Void.class); + + server2.verify(Duration.ofMillis(100)); + } + }