|
18 | 18 |
|
19 | 19 | import java.util.Map;
|
20 | 20 | import java.util.function.Consumer;
|
21 |
| -import java.util.stream.Stream; |
22 | 21 |
|
23 | 22 | import org.junit.Test;
|
24 | 23 | import org.reactivestreams.Publisher;
|
|
28 | 27 | import org.springframework.core.ResolvableType;
|
29 | 28 | import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase;
|
30 | 29 | import org.springframework.core.io.buffer.DataBuffer;
|
31 |
| -import org.springframework.core.io.buffer.DataBufferFactory; |
32 | 30 | import org.springframework.core.io.buffer.DataBufferUtils;
|
33 | 31 | import org.springframework.lang.Nullable;
|
34 | 32 | import org.springframework.util.Assert;
|
35 | 33 | import org.springframework.util.MimeType;
|
36 | 34 |
|
37 | 35 | import static java.nio.charset.StandardCharsets.UTF_8;
|
38 | 36 | import static org.junit.Assert.*;
|
| 37 | +import static org.springframework.core.io.buffer.DataBufferUtils.release; |
39 | 38 |
|
40 | 39 | /**
|
41 | 40 | * 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}. |
44 | 43 | *
|
45 | 44 | * @author Arjen Poutsma
|
| 45 | + * @since 5.1.3 |
46 | 46 | */
|
47 | 47 | @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 { |
50 | 50 |
|
51 | 51 | /**
|
52 | 52 | * The encoder to test.
|
53 | 53 | */
|
54 | 54 | protected final E encoder;
|
55 | 55 |
|
| 56 | + |
56 | 57 | /**
|
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 |
59 | 60 | */
|
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 | + |
61 | 68 |
|
62 | 69 | /**
|
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}. |
66 | 71 | */
|
67 |
| - @Nullable |
68 |
| - protected final MimeType mimeType; |
| 72 | + @Test |
| 73 | + public abstract void canEncode() throws Exception; |
69 | 74 |
|
70 | 75 | /**
|
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. |
74 | 78 | */
|
75 |
| - @Nullable |
76 |
| - protected final Map<String, Object> hints; |
| 79 | + @Test |
| 80 | + public abstract void encode() throws Exception; |
77 | 81 |
|
78 | 82 |
|
79 | 83 | /**
|
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 |
83 | 97 | */
|
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); |
86 | 101 | }
|
87 | 102 |
|
88 | 103 | /**
|
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 |
94 | 119 | */
|
95 |
| - protected AbstractEncoderTestCase(E encoder, ResolvableType elementType, |
| 120 | + protected <T> void testEncodeAll(Publisher<? extends T> input, ResolvableType inputType, |
| 121 | + Consumer<StepVerifier.FirstStep<DataBuffer>> stepConsumer, |
96 | 122 | @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 | + } |
97 | 128 |
|
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 | + } |
100 | 141 |
|
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); |
105 | 161 | }
|
106 | 162 |
|
107 | 163 | /**
|
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 |
110 | 174 | */
|
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 | + } |
112 | 190 |
|
113 | 191 | /**
|
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}. |
117 | 201 | */
|
118 |
| - protected abstract Stream<Consumer<DataBuffer>> outputConsumers(); |
| 202 | + protected void testEncodeCancel(Publisher<?> input, ResolvableType inputType, |
| 203 | + @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { |
119 | 204 |
|
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(); |
123 | 212 | }
|
124 | 213 |
|
125 | 214 | /**
|
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}. |
129 | 221 | */
|
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) { |
137 | 224 |
|
| 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(); |
138 | 231 | }
|
139 | 232 |
|
140 | 233 | /**
|
141 | 234 | * Create a result consumer that expects the given bytes.
|
142 |
| - * @param expected the expected string |
| 235 | + * @param expected the expected bytes |
143 | 236 | * @return a consumer that expects the given data buffer to be equal to {@code expected}
|
144 | 237 | */
|
145 |
| - protected final Consumer<DataBuffer> resultConsumer(byte[] expected) { |
| 238 | + protected final Consumer<DataBuffer> expectBytes(byte[] expected) { |
146 | 239 | return dataBuffer -> {
|
147 | 240 | byte[] resultBytes = new byte[dataBuffer.readableByteCount()];
|
148 | 241 | dataBuffer.read(resultBytes);
|
| 242 | + release(dataBuffer); |
149 | 243 | assertArrayEquals(expected, resultBytes);
|
150 | 244 | };
|
151 | 245 | }
|
152 | 246 |
|
153 | 247 | /**
|
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} |
156 | 251 | */
|
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 | + }; |
167 | 260 |
|
168 |
| - step.expectComplete() |
169 |
| - .verify(); |
170 | 261 | }
|
171 | 262 |
|
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 | + |
207 | 267 | }
|
208 | 268 |
|
209 | 269 | /**
|
210 |
| - * Tests whether canceling the output of the encoder can be consumed with |
211 |
| - * {@link #outputConsumers()}. |
| 270 | + * Exception used in {@link #testEncodeError}. |
212 | 271 | */
|
213 |
| - @Test |
214 |
| - public final void encodeCancel() { |
215 |
| - Flux<T> input = input(); |
| 272 | + @SuppressWarnings("serial") |
| 273 | + public static class InputException extends RuntimeException { |
216 | 274 |
|
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(); |
226 | 275 | }
|
227 | 276 |
|
| 277 | + |
228 | 278 | }
|
0 commit comments