Skip to content

Commit 6f6fb82

Browse files
authored
Attempt to make WaitAllStrategyTests more stable (testcontainers#1400)
@kiview WDYT? I was finding that these tests were failing randomly ~30% of the time, which is likely to be caused by the specific timing-sensitive aspects of the code. To be honest I also found the existing tests (the ones which exercised the timeouts behaviour) to be hard to follow, and therefore very hard to be sure that it would be both correct and stable. So, instead, I've updated the test: * to keep the existing mock-based tests to check _what the strategy does_ (agnostic of timeout durations). * to replace the timing-sensitive tests with some simpler tests that just check that the `startupTimeout`s get propagated properly. These tests _don't_ fire the `WaitAllStrategy`, i.e. don't actually wait at all. My thinking is that the combination of these two types of tests really should be enough. 'Integration testing' this would be nice, but is complicated and error prone, so testing the execution flow and the data flow, essentially, is a compromise that I'm happier with.
1 parent 92fc7fa commit 6f6fb82

File tree

1 file changed

+55
-106
lines changed

1 file changed

+55
-106
lines changed
Lines changed: 55 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
package org.testcontainers.containers.wait.strategy;
22

3-
import static org.mockito.ArgumentMatchers.*;
4-
import static org.mockito.Mockito.*;
5-
import static org.rnorth.visibleassertions.VisibleAssertions.*;
6-
7-
import java.time.Duration;
8-
import java.util.concurrent.CompletableFuture;
9-
import java.util.concurrent.ExecutionException;
10-
import java.util.concurrent.TimeUnit;
11-
123
import org.junit.Before;
134
import org.junit.Test;
145
import org.mockito.InOrder;
@@ -17,7 +8,12 @@
178
import org.rnorth.ducttape.TimeoutException;
189
import org.testcontainers.containers.GenericContainer;
1910

20-
import com.google.common.util.concurrent.Uninterruptibles;
11+
import java.time.Duration;
12+
13+
import static org.mockito.ArgumentMatchers.any;
14+
import static org.mockito.ArgumentMatchers.eq;
15+
import static org.mockito.Mockito.*;
16+
import static org.rnorth.visibleassertions.VisibleAssertions.*;
2117

2218
public class WaitAllStrategyTest {
2319

@@ -35,80 +31,78 @@ public void setUp() {
3531
MockitoAnnotations.initMocks(this);
3632
}
3733

34+
/*
35+
* Dummy-based tests, to check that timeout values are propagated correctly, without involving actual timing-sensitive code
36+
*/
3837
@Test
39-
public void simpleTest() {
40-
41-
final WaitStrategy underTest = new WaitAllStrategy()
42-
.withStrategy(strategy1)
43-
.withStrategy(strategy2);
44-
45-
doNothing().when(strategy1).waitUntilReady(eq(container));
46-
doNothing().when(strategy2).waitUntilReady(eq(container));
47-
48-
underTest.waitUntilReady(container);
49-
50-
InOrder inOrder = inOrder(strategy1, strategy2);
51-
inOrder.verify(strategy1).waitUntilReady(any());
52-
inOrder.verify(strategy2).waitUntilReady(any());
53-
}
38+
public void parentTimeoutApplies() {
5439

55-
@Test
56-
public void maxTimeOutApplies() {
40+
DummyStrategy child1 = new DummyStrategy(Duration.ofMillis(10));
41+
child1.withStartupTimeout(Duration.ofMillis(20));
5742

58-
WaitStrategy child1 = new SleepingStrategy(Duration.ofMinutes(30))
59-
.withStartupTimeout(Duration.ofHours(1));
43+
assertEquals("withStartupTimeout directly sets the timeout", 20L, child1.startupTimeout.toMillis());
6044

61-
final WaitStrategy underTest = new WaitAllStrategy()
45+
new WaitAllStrategy()
6246
.withStrategy(child1)
63-
.withStartupTimeout(Duration.ofMillis(10));
47+
.withStartupTimeout(Duration.ofMillis(30));
6448

65-
assertThrows("The outer strategy timeout applies", TimeoutException.class, () -> {
66-
underTest.waitUntilReady(container);
67-
});
49+
assertEquals("WaitAllStrategy overrides a child's timeout", 30L, child1.startupTimeout.toMillis());
6850
}
6951

7052
@Test
71-
public void appliesOuterTimeoutTooLittleTime() {
53+
public void parentTimeoutAppliesToMultipleChildren() {
7254

7355
Duration defaultInnerWait = Duration.ofMillis(2);
7456
Duration outerWait = Duration.ofMillis(6);
7557

76-
WaitStrategy child1 = new SleepingStrategy(defaultInnerWait);
77-
WaitStrategy child2 = new SleepingStrategy(defaultInnerWait);
58+
DummyStrategy child1 = new DummyStrategy(defaultInnerWait);
59+
DummyStrategy child2 = new DummyStrategy(defaultInnerWait);
7860

79-
final WaitStrategy underTest = new WaitAllStrategy()
61+
new WaitAllStrategy()
8062
.withStrategy(child1)
8163
.withStrategy(child2)
8264
.withStartupTimeout(outerWait);
8365

84-
assertThrows("The outer strategy timeout applies", TimeoutException.class, () -> {
85-
underTest.waitUntilReady(container);
86-
});
66+
assertEquals("WaitAllStrategy overrides a child's timeout (1st)", 6L, child1.startupTimeout.toMillis());
67+
assertEquals("WaitAllStrategy overrides a child's timeout (2nd)", 6L, child2.startupTimeout.toMillis());
8768
}
8869

8970
@Test
90-
public void appliesOuterTimeoutEnoughTime() {
71+
public void parentTimeoutAppliesToAdditionalChildren() {
9172

9273
Duration defaultInnerWait = Duration.ofMillis(2);
9374
Duration outerWait = Duration.ofMillis(20);
9475

95-
WaitStrategy child1 = new SleepingStrategy(defaultInnerWait);
96-
WaitStrategy child2 = new SleepingStrategy(defaultInnerWait);
76+
DummyStrategy child1 = new DummyStrategy(defaultInnerWait);
77+
DummyStrategy child2 = new DummyStrategy(defaultInnerWait);
9778

98-
final WaitStrategy underTest = new WaitAllStrategy()
79+
new WaitAllStrategy()
9980
.withStrategy(child1)
100-
.withStrategy(child2)
101-
.withStartupTimeout(outerWait);
81+
.withStartupTimeout(outerWait)
82+
.withStrategy(child2);
10283

103-
try {
104-
underTest.waitUntilReady(container);
105-
} catch (Exception e) {
106-
if (e.getCause() instanceof CaughtTimeoutException)
107-
fail("The timeout wasn't applied to inner strategies.");
108-
else {
109-
fail(e.getMessage());
110-
}
111-
}
84+
assertEquals("WaitAllStrategy overrides a child's timeout (1st)", 20L, child1.startupTimeout.toMillis());
85+
assertEquals("WaitAllStrategy overrides a child's timeout (2nd, additional)", 20L, child2.startupTimeout.toMillis());
86+
}
87+
88+
/*
89+
* Mock-based tests to check overall behaviour, without involving timing-sensitive code
90+
*/
91+
@Test
92+
public void childExecutionTest() {
93+
94+
final WaitStrategy underTest = new WaitAllStrategy()
95+
.withStrategy(strategy1)
96+
.withStrategy(strategy2);
97+
98+
doNothing().when(strategy1).waitUntilReady(eq(container));
99+
doNothing().when(strategy2).waitUntilReady(eq(container));
100+
101+
underTest.waitUntilReady(container);
102+
103+
InOrder inOrder = inOrder(strategy1, strategy2);
104+
inOrder.verify(strategy1).waitUntilReady(any());
105+
inOrder.verify(strategy2).waitUntilReady(any());
112106
}
113107

114108
@Test
@@ -145,7 +139,7 @@ public void timeoutChangeShouldNotBePossibleWithIndividualTimeoutMode() {
145139
@Test
146140
public void shouldNotMessWithIndividualTimeouts() {
147141

148-
final WaitStrategy underTest = new WaitAllStrategy(WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY)
142+
new WaitAllStrategy(WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY)
149143
.withStrategy(strategy1)
150144
.withStrategy(strategy2);
151145

@@ -157,7 +151,7 @@ public void shouldNotMessWithIndividualTimeouts() {
157151
public void shouldOverwriteIndividualTimeouts() {
158152

159153
Duration someSeconds = Duration.ofSeconds(23);
160-
final WaitStrategy underTest = new WaitAllStrategy()
154+
new WaitAllStrategy()
161155
.withStartupTimeout(someSeconds)
162156
.withStrategy(strategy1)
163157
.withStrategy(strategy2);
@@ -166,59 +160,14 @@ public void shouldOverwriteIndividualTimeouts() {
166160
verify(strategy1).withStartupTimeout(someSeconds);
167161
}
168162

169-
@Test
170-
public void appliesOuterTimeoutOnAdditionalChildren() {
171-
172-
Duration defaultInnerWait = Duration.ofMillis(2);
173-
Duration outerWait = Duration.ofMillis(20);
174-
175-
WaitStrategy child1 = new SleepingStrategy(defaultInnerWait);
176-
WaitStrategy child2 = new SleepingStrategy(defaultInnerWait);
177-
178-
final WaitStrategy underTest = new WaitAllStrategy()
179-
.withStrategy(child1)
180-
.withStartupTimeout(outerWait)
181-
.withStrategy(child2);
182-
183-
try {
184-
underTest.waitUntilReady(container);
185-
} catch (Exception e) {
186-
if (e.getCause() instanceof CaughtTimeoutException)
187-
fail("The timeout wasn't applied to inner strategies.");
188-
else {
189-
fail(e.getMessage());
190-
}
191-
}
192-
}
193-
194-
static class CaughtTimeoutException extends RuntimeException {
195-
196-
CaughtTimeoutException(String message) {
197-
super(message);
198-
}
199-
}
200-
201-
static class SleepingStrategy extends AbstractWaitStrategy {
202-
203-
private final long sleepingDuration;
204-
205-
SleepingStrategy(Duration defaultInnerWait) {
163+
static class DummyStrategy extends AbstractWaitStrategy {
164+
DummyStrategy(Duration defaultInnerWait) {
206165
super.startupTimeout = defaultInnerWait;
207-
// Always oversleep by default
208-
this.sleepingDuration = defaultInnerWait.multipliedBy(2).toMillis();
209166
}
210167

211168
@Override
212169
protected void waitUntilReady() {
213-
try {
214-
// Don't use ducttape, make sure we don't catch the wrong TimeoutException later on.
215-
CompletableFuture.runAsync(
216-
() -> Uninterruptibles.sleepUninterruptibly(this.sleepingDuration, TimeUnit.MILLISECONDS)
217-
).get((int) startupTimeout.toMillis(), TimeUnit.MILLISECONDS);
218-
} catch (InterruptedException | ExecutionException e) {
219-
} catch (java.util.concurrent.TimeoutException e) {
220-
throw new CaughtTimeoutException("Inner wait timed out, outer strategy didn't apply.");
221-
}
170+
// no-op
222171
}
223172
}
224173
}

0 commit comments

Comments
 (0)