Skip to content

Commit 539cfc2

Browse files
committed
Refactor AbstractEncoderTestCase
Refactor AbstractEncoderTestCase to resemble AbstractDecoderTestCase Issue: SPR-17449
1 parent 39ce989 commit 539cfc2

File tree

10 files changed

+458
-359
lines changed

10 files changed

+458
-359
lines changed

spring-core/src/test/java/org/springframework/core/codec/AbstractEncoderTestCase.java

Lines changed: 171 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.Map;
2020
import java.util.function.Consumer;
21-
import java.util.stream.Stream;
2221

2322
import org.junit.Test;
2423
import org.reactivestreams.Publisher;
@@ -28,201 +27,252 @@
2827
import org.springframework.core.ResolvableType;
2928
import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase;
3029
import org.springframework.core.io.buffer.DataBuffer;
31-
import org.springframework.core.io.buffer.DataBufferFactory;
3230
import org.springframework.core.io.buffer.DataBufferUtils;
3331
import org.springframework.lang.Nullable;
3432
import org.springframework.util.Assert;
3533
import org.springframework.util.MimeType;
3634

3735
import static java.nio.charset.StandardCharsets.UTF_8;
3836
import static org.junit.Assert.*;
37+
import static org.springframework.core.io.buffer.DataBufferUtils.release;
3938

4039
/**
4140
* Abstract base class for {@link Encoder} unit tests. Subclasses need to implement
42-
* {@link #input()} and {@link #outputConsumers()}, from which {@link #encode()},
43-
* {@link #encodeError()} and {@link #encodeCancel()} are run.
41+
* {@link #canEncode()} and {@link #encode()}, possibly using the wide
42+
* * variety of helper methods like {@link #testEncodeAll}.
4443
*
4544
* @author Arjen Poutsma
45+
* @since 5.1.3
4646
*/
4747
@SuppressWarnings("ProtectedField")
48-
public abstract class AbstractEncoderTestCase<T, E extends Encoder<T>> extends
49-
AbstractLeakCheckingTestCase {
48+
public abstract class AbstractEncoderTestCase<E extends Encoder<?>>
49+
extends AbstractLeakCheckingTestCase {
5050

5151
/**
5252
* The encoder to test.
5353
*/
5454
protected final E encoder;
5555

56+
5657
/**
57-
* The type used for
58-
* {@link Encoder#encode(Publisher, DataBufferFactory, ResolvableType, MimeType, Map)}.
58+
* Construct a new {@code AbstractEncoderTestCase} for the given parameters.
59+
* @param encoder the encoder
5960
*/
60-
protected final ResolvableType elementType;
61+
protected AbstractEncoderTestCase(E encoder) {
62+
63+
Assert.notNull(encoder, "Encoder must not be null");
64+
65+
this.encoder = encoder;
66+
}
67+
6168

6269
/**
63-
* The mime type used for
64-
* {@link Encoder#encode(Publisher, DataBufferFactory, ResolvableType, MimeType, Map)}.
65-
* May be {@code null}.
70+
* Subclasses should implement this method to test {@link Encoder#canEncode}.
6671
*/
67-
@Nullable
68-
protected final MimeType mimeType;
72+
@Test
73+
public abstract void canEncode() throws Exception;
6974

7075
/**
71-
* The hints used for
72-
* {@link Encoder#encode(Publisher, DataBufferFactory, ResolvableType, MimeType, Map)}.
73-
* May be {@code null}.
76+
* Subclasses should implement this method to test {@link Encoder#encode}, possibly using
77+
* {@link #testEncodeAll} or other helper methods.
7478
*/
75-
@Nullable
76-
protected final Map<String, Object> hints;
79+
@Test
80+
public abstract void encode() throws Exception;
7781

7882

7983
/**
80-
* Construct a new {@code AbstractEncoderTestCase} for the given encoder and element class.
81-
* @param encoder the encoder
82-
* @param elementClass the element class
84+
* Helper methods that tests for a variety of encoding scenarios. This methods
85+
* invokes:
86+
* <ul>
87+
* <li>{@link #testEncode(Publisher, ResolvableType, Consumer, MimeType, Map)}</li>
88+
* <li>{@link #testEncodeError(Publisher, ResolvableType, MimeType, Map)}</li>
89+
* <li>{@link #testEncodeCancel(Publisher, ResolvableType, MimeType, Map)}</li>
90+
* <li>{@link #testEncodeEmpty(ResolvableType, MimeType, Map)}</li>
91+
* </ul>
92+
*
93+
* @param input the input to be provided to the encoder
94+
* @param inputClass the input class
95+
* @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output
96+
* @param <T> the output type
8397
*/
84-
protected AbstractEncoderTestCase(E encoder, Class<?> elementClass) {
85-
this(encoder, ResolvableType.forClass(elementClass), null, null);
98+
protected <T> void testEncodeAll(Publisher<? extends T> input, Class<? extends T> inputClass,
99+
Consumer<StepVerifier.FirstStep<DataBuffer>> stepConsumer) {
100+
testEncodeAll(input, ResolvableType.forClass(inputClass), stepConsumer, null, null);
86101
}
87102

88103
/**
89-
* Construct a new {@code AbstractEncoderTestCase} for the given parameters.
90-
* @param encoder the encoder
91-
* @param elementType the element type
92-
* @param mimeType the mime type. May be {@code null}.
93-
* @param hints the hints. May be {@code null}.
104+
* Helper methods that tests for a variety of decoding scenarios. This methods
105+
* invokes:
106+
* <ul>
107+
* <li>{@link #testEncode(Publisher, ResolvableType, Consumer, MimeType, Map)}</li>
108+
* <li>{@link #testEncodeError(Publisher, ResolvableType, MimeType, Map)}</li>
109+
* <li>{@link #testEncodeCancel(Publisher, ResolvableType, MimeType, Map)}</li>
110+
* <li>{@link #testEncodeEmpty(ResolvableType, MimeType, Map)}</li>
111+
* </ul>
112+
*
113+
* @param input the input to be provided to the encoder
114+
* @param inputType the input type
115+
* @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output
116+
* @param mimeType the mime type to use for decoding. May be {@code null}.
117+
* @param hints the hints used for decoding. May be {@code null}.
118+
* @param <T> the output type
94119
*/
95-
protected AbstractEncoderTestCase(E encoder, ResolvableType elementType,
120+
protected <T> void testEncodeAll(Publisher<? extends T> input, ResolvableType inputType,
121+
Consumer<StepVerifier.FirstStep<DataBuffer>> stepConsumer,
96122
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
123+
testEncode(input, inputType, stepConsumer, mimeType, hints);
124+
testEncodeError(input, inputType, mimeType, hints);
125+
testEncodeCancel(input, inputType, mimeType, hints);
126+
testEncodeEmpty(inputType, mimeType, hints);
127+
}
97128

98-
Assert.notNull(encoder, "Encoder must not be null");
99-
Assert.notNull(elementType, "ElementType must not be null");
129+
/**
130+
* Test a standard {@link Encoder#encode encode} scenario.
131+
*
132+
* @param input the input to be provided to the encoder
133+
* @param inputClass the input class
134+
* @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output
135+
* @param <T> the output type
136+
*/
137+
protected <T> void testEncode(Publisher<? extends T> input, Class<? extends T> inputClass,
138+
Consumer<StepVerifier.FirstStep<DataBuffer>> stepConsumer) {
139+
testEncode(input, ResolvableType.forClass(inputClass), stepConsumer, null, null);
140+
}
100141

101-
this.encoder = encoder;
102-
this.elementType = elementType;
103-
this.mimeType = mimeType;
104-
this.hints = hints;
142+
/**
143+
* Test a standard {@link Encoder#encode encode} scenario.
144+
*
145+
* @param input the input to be provided to the encoder
146+
* @param inputType the input type
147+
* @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output
148+
* @param mimeType the mime type to use for decoding. May be {@code null}.
149+
* @param hints the hints used for decoding. May be {@code null}.
150+
* @param <T> the output type
151+
*/
152+
@SuppressWarnings("unchecked")
153+
protected <T> void testEncode(Publisher<? extends T> input, ResolvableType inputType,
154+
Consumer<StepVerifier.FirstStep<DataBuffer>> stepConsumer,
155+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
156+
157+
Flux<DataBuffer> result = encoder().encode(input, this.bufferFactory, inputType,
158+
mimeType, hints);
159+
StepVerifier.FirstStep<DataBuffer> step = StepVerifier.create(result);
160+
stepConsumer.accept(step);
105161
}
106162

107163
/**
108-
* Abstract template method that provides input for the encoder.
109-
* Used for {@link #encode()}, {@link #encodeError()}, and {@link #encodeCancel()}.
164+
* Test a {@link Encoder#encode encode} scenario where the input stream contains an error.
165+
* This test method will feed the first element of the {@code input} stream to the encoder,
166+
* followed by an {@link InputException}.
167+
* The result is expected to contain one "normal" element, followed by the error.
168+
*
169+
* @param input the input to be provided to the encoder
170+
* @param inputType the input type
171+
* @param mimeType the mime type to use for decoding. May be {@code null}.
172+
* @param hints the hints used for decoding. May be {@code null}.
173+
* @see InputException
110174
*/
111-
protected abstract Flux<T> input();
175+
protected void testEncodeError(Publisher<?> input, ResolvableType inputType,
176+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
177+
178+
input = Flux.concat(
179+
Flux.from(input).take(1),
180+
Flux.error(new InputException()));
181+
182+
Flux<DataBuffer> result = encoder().encode(input, this.bufferFactory, inputType,
183+
mimeType, hints);
184+
185+
StepVerifier.create(result)
186+
.consumeNextWith(DataBufferUtils::release)
187+
.expectError(InputException.class)
188+
.verify();
189+
}
112190

113191
/**
114-
* Abstract template method that verifies the output of the encoder.
115-
* The returned stream should contain a buffer consumer for each expected output, given
116-
* the {@linkplain #input()}.
192+
* Test a {@link Encoder#encode encode} scenario where the input stream is canceled.
193+
* This test method will feed the first element of the {@code input} stream to the decoder,
194+
* followed by a cancel signal.
195+
* The result is expected to contain one "normal" element.
196+
*
197+
* @param input the input to be provided to the encoder
198+
* @param inputType the input type
199+
* @param mimeType the mime type to use for decoding. May be {@code null}.
200+
* @param hints the hints used for decoding. May be {@code null}.
117201
*/
118-
protected abstract Stream<Consumer<DataBuffer>> outputConsumers();
202+
protected void testEncodeCancel(Publisher<?> input, ResolvableType inputType,
203+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
119204

120-
private Stream<Consumer<DataBuffer>> outputAndReleaseConsumers() {
121-
return outputConsumers()
122-
.map(consumer -> consumer.andThen(DataBufferUtils::release));
205+
Flux<DataBuffer> result = encoder().encode(input, this.bufferFactory, inputType, mimeType,
206+
hints);
207+
208+
StepVerifier.create(result)
209+
.consumeNextWith(DataBufferUtils::release)
210+
.thenCancel()
211+
.verify();
123212
}
124213

125214
/**
126-
* Create a result consumer that expects the given String in UTF-8 encoding.
127-
* @param expected the expected string
128-
* @return a consumer that expects the given data buffer to be equal to {@code expected}
215+
* Test a {@link Encoder#encode encode} scenario where the input stream is empty.
216+
* The output is expected to be empty as well.
217+
*
218+
* @param inputType the input type
219+
* @param mimeType the mime type to use for decoding. May be {@code null}.
220+
* @param hints the hints used for decoding. May be {@code null}.
129221
*/
130-
protected final Consumer<DataBuffer> resultConsumer(String expected) {
131-
return dataBuffer -> {
132-
byte[] resultBytes = new byte[dataBuffer.readableByteCount()];
133-
dataBuffer.read(resultBytes);
134-
String actual = new String(resultBytes, UTF_8);
135-
assertEquals(expected, actual);
136-
};
222+
protected void testEncodeEmpty(ResolvableType inputType, @Nullable MimeType mimeType,
223+
@Nullable Map<String, Object> hints) {
137224

225+
Flux<?> input = Flux.empty();
226+
Flux<DataBuffer> result = encoder().encode(input, this.bufferFactory, inputType,
227+
mimeType, hints);
228+
229+
StepVerifier.create(result)
230+
.verifyComplete();
138231
}
139232

140233
/**
141234
* Create a result consumer that expects the given bytes.
142-
* @param expected the expected string
235+
* @param expected the expected bytes
143236
* @return a consumer that expects the given data buffer to be equal to {@code expected}
144237
*/
145-
protected final Consumer<DataBuffer> resultConsumer(byte[] expected) {
238+
protected final Consumer<DataBuffer> expectBytes(byte[] expected) {
146239
return dataBuffer -> {
147240
byte[] resultBytes = new byte[dataBuffer.readableByteCount()];
148241
dataBuffer.read(resultBytes);
242+
release(dataBuffer);
149243
assertArrayEquals(expected, resultBytes);
150244
};
151245
}
152246

153247
/**
154-
* Tests whether passing {@link #input()} to the encoder can be consumed with
155-
* {@link #outputConsumers()}.
248+
* Create a result consumer that expects the given string, using the UTF-8 encoding.
249+
* @param expected the expected string
250+
* @return a consumer that expects the given data buffer to be equal to {@code expected}
156251
*/
157-
@Test
158-
public final void encode() {
159-
Flux<T> input = input();
160-
161-
Flux<DataBuffer> output = this.encoder.encode(input, this.bufferFactory,
162-
this.elementType, this.mimeType, this.hints);
163-
164-
StepVerifier.Step<DataBuffer> step = StepVerifier.create(output);
165-
166-
outputAndReleaseConsumers().forEach(step::consumeNextWith);
252+
protected Consumer<DataBuffer> expectString(String expected) {
253+
return dataBuffer -> {
254+
byte[] resultBytes = new byte[dataBuffer.readableByteCount()];
255+
dataBuffer.read(resultBytes);
256+
release(dataBuffer);
257+
String actual = new String(resultBytes, UTF_8);
258+
assertEquals(expected, actual);
259+
};
167260

168-
step.expectComplete()
169-
.verify();
170261
}
171262

172-
/**
173-
* Tests whether passing an error to the encoder can be consumed with
174-
* {@link #outputConsumers()}.
175-
*/
176-
@Test
177-
public final void encodeError() {
178-
179-
boolean singleValue = this.encoder instanceof AbstractSingleValueEncoder;
180-
181-
Flux<T> input;
182-
if (singleValue) {
183-
input = Flux.error(new RuntimeException());
184-
}
185-
else {
186-
input = Flux.concat(
187-
input().take(1),
188-
Flux.error(new RuntimeException()));
189-
}
190-
191-
Flux<DataBuffer> output = this.encoder.encode(input, this.bufferFactory,
192-
this.elementType, this.mimeType, this.hints);
193-
194-
if (singleValue) {
195-
StepVerifier.create(output)
196-
.expectError(RuntimeException.class)
197-
.verify();
198-
}
199-
else {
200-
Consumer<DataBuffer> firstResultConsumer = outputAndReleaseConsumers().findFirst()
201-
.orElseThrow(IllegalArgumentException::new);
202-
StepVerifier.create(output)
203-
.consumeNextWith(firstResultConsumer)
204-
.expectError(RuntimeException.class)
205-
.verify();
206-
}
263+
@SuppressWarnings("unchecked")
264+
private <T> Encoder<T> encoder() {
265+
return (Encoder<T>) this.encoder;
266+
207267
}
208268

209269
/**
210-
* Tests whether canceling the output of the encoder can be consumed with
211-
* {@link #outputConsumers()}.
270+
* Exception used in {@link #testEncodeError}.
212271
*/
213-
@Test
214-
public final void encodeCancel() {
215-
Flux<T> input = input();
272+
@SuppressWarnings("serial")
273+
public static class InputException extends RuntimeException {
216274

217-
Flux<DataBuffer> output = this.encoder.encode(input, this.bufferFactory,
218-
this.elementType, this.mimeType, this.hints);
219-
220-
Consumer<DataBuffer> firstResultConsumer = outputAndReleaseConsumers().findFirst()
221-
.orElseThrow(IllegalArgumentException::new);
222-
StepVerifier.create(output)
223-
.consumeNextWith(firstResultConsumer)
224-
.thenCancel()
225-
.verify();
226275
}
227276

277+
228278
}

0 commit comments

Comments
 (0)