Skip to content

Commit 9487817

Browse files
authored
test: Add comprehensive edge case and validation tests for core Spring AI builders (#4034)
Adds comprehensive test coverage across 3 critical Spring AI builder components: - PromptTemplateBuilderTests - FilterExpressionBuilderTests - DocumentBuilderTests Signed-off-by: Alex Klimenko <[email protected]>
1 parent 45de6c7 commit 9487817

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed

spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentBuilderTests.java

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,134 @@ void testBuildWithAllProperties() {
160160
assertThat(document.getMetadata()).isEqualTo(metadata);
161161
}
162162

163+
@Test
164+
void testWithWhitespaceOnlyId() {
165+
assertThatThrownBy(() -> this.builder.text("text").id(" ").build())
166+
.isInstanceOf(IllegalArgumentException.class)
167+
.hasMessageContaining("id cannot be null or empty");
168+
}
169+
170+
@Test
171+
void testWithEmptyText() {
172+
Document document = this.builder.text("").build();
173+
assertThat(document.getText()).isEqualTo("");
174+
}
175+
176+
@Test
177+
void testOverwritingText() {
178+
Document document = this.builder.text("initial text").text("final text").build();
179+
assertThat(document.getText()).isEqualTo("final text");
180+
}
181+
182+
@Test
183+
void testMultipleMetadataKeyValueCalls() {
184+
Document document = this.builder.text("text")
185+
.metadata("key1", "value1")
186+
.metadata("key2", "value2")
187+
.metadata("key3", 123)
188+
.build();
189+
190+
assertThat(document.getMetadata()).hasSize(3)
191+
.containsEntry("key1", "value1")
192+
.containsEntry("key2", "value2")
193+
.containsEntry("key3", 123);
194+
}
195+
196+
@Test
197+
void testMetadataMapOverridesKeyValue() {
198+
Map<String, Object> metadata = new HashMap<>();
199+
metadata.put("newKey", "newValue");
200+
201+
Document document = this.builder.text("text").metadata("oldKey", "oldValue").metadata(metadata).build();
202+
203+
assertThat(document.getMetadata()).hasSize(1).containsEntry("newKey", "newValue").doesNotContainKey("oldKey");
204+
}
205+
206+
@Test
207+
void testKeyValueMetadataAfterMap() {
208+
Map<String, Object> metadata = new HashMap<>();
209+
metadata.put("mapKey", "mapValue");
210+
211+
Document document = this.builder.text("text")
212+
.metadata(metadata)
213+
.metadata("additionalKey", "additionalValue")
214+
.build();
215+
216+
assertThat(document.getMetadata()).hasSize(2)
217+
.containsEntry("mapKey", "mapValue")
218+
.containsEntry("additionalKey", "additionalValue");
219+
}
220+
221+
@Test
222+
void testWithEmptyMetadataMap() {
223+
Map<String, Object> emptyMetadata = new HashMap<>();
224+
225+
Document document = this.builder.text("text").metadata(emptyMetadata).build();
226+
227+
assertThat(document.getMetadata()).isEmpty();
228+
}
229+
230+
@Test
231+
void testOverwritingMetadataWithSameKey() {
232+
Document document = this.builder.text("text")
233+
.metadata("key", "firstValue")
234+
.metadata("key", "secondValue")
235+
.build();
236+
237+
assertThat(document.getMetadata()).hasSize(1).containsEntry("key", "secondValue");
238+
}
239+
240+
@Test
241+
void testWithNullMedia() {
242+
Document document = this.builder.text("text").media(null).build();
243+
assertThat(document.getMedia()).isNull();
244+
}
245+
246+
@Test
247+
void testIdOverridesIdGenerator() {
248+
IdGenerator generator = contents -> "generated-id";
249+
250+
Document document = this.builder.text("text").idGenerator(generator).id("explicit-id").build();
251+
252+
assertThat(document.getId()).isEqualTo("explicit-id");
253+
}
254+
255+
@Test
256+
void testComplexMetadataTypes() {
257+
Map<String, Object> nestedMap = new HashMap<>();
258+
nestedMap.put("nested", "value");
259+
260+
Document document = this.builder.text("text")
261+
.metadata("string", "text")
262+
.metadata("integer", 42)
263+
.metadata("double", 3.14)
264+
.metadata("boolean", true)
265+
.metadata("map", nestedMap)
266+
.build();
267+
268+
assertThat(document.getMetadata()).hasSize(5)
269+
.containsEntry("string", "text")
270+
.containsEntry("integer", 42)
271+
.containsEntry("double", 3.14)
272+
.containsEntry("boolean", true)
273+
.containsEntry("map", nestedMap);
274+
}
275+
276+
@Test
277+
void testBuilderReuse() {
278+
// First document
279+
Document doc1 = this.builder.text("first").id("id1").metadata("key", "value1").build();
280+
281+
// Reuse builder for second document
282+
Document doc2 = this.builder.text("second").id("id2").metadata("key", "value2").build();
283+
284+
assertThat(doc1.getId()).isEqualTo("id1");
285+
assertThat(doc1.getText()).isEqualTo("first");
286+
assertThat(doc1.getMetadata()).containsEntry("key", "value1");
287+
288+
assertThat(doc2.getId()).isEqualTo("id2");
289+
assertThat(doc2.getText()).isEqualTo("second");
290+
assertThat(doc2.getMetadata()).containsEntry("key", "value2");
291+
}
292+
163293
}

spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/PromptTemplateBuilderTests.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,101 @@ void renderWithMissingVariableShouldThrow() {
9696
}
9797
}
9898

99+
@Test
100+
void builderWithWhitespaceOnlyTemplateShouldThrow() {
101+
assertThatThrownBy(() -> PromptTemplate.builder().template(" ")).isInstanceOf(IllegalArgumentException.class)
102+
.hasMessageContaining("template cannot be null or empty");
103+
}
104+
105+
@Test
106+
void builderWithEmptyVariablesMapShouldWork() {
107+
Map<String, Object> emptyVariables = new HashMap<>();
108+
PromptTemplate promptTemplate = PromptTemplate.builder()
109+
.template("Status: active")
110+
.variables(emptyVariables)
111+
.build();
112+
113+
assertThat(promptTemplate.render()).isEqualTo("Status: active");
114+
}
115+
116+
@Test
117+
void builderNullVariableValueShouldWork() {
118+
Map<String, Object> variables = new HashMap<>();
119+
variables.put("value", null);
120+
121+
PromptTemplate promptTemplate = PromptTemplate.builder()
122+
.template("Result: {value}")
123+
.variables(variables)
124+
.build();
125+
126+
// Should handle null values gracefully
127+
String result = promptTemplate.render();
128+
assertThat(result).contains("Result:").contains(":");
129+
}
130+
131+
@Test
132+
void builderWithMultipleMissingVariablesShouldThrow() {
133+
PromptTemplate promptTemplate = PromptTemplate.builder()
134+
.template("Processing {item} with {type} at {level}")
135+
.build();
136+
137+
try {
138+
promptTemplate.render();
139+
Assertions.fail("Expected IllegalStateException was not thrown.");
140+
}
141+
catch (IllegalStateException e) {
142+
assertThat(e.getMessage()).contains("Not all variables were replaced in the template");
143+
assertThat(e.getMessage()).contains("item", "type", "level");
144+
}
145+
}
146+
147+
@Test
148+
void builderWithPartialVariablesShouldThrow() {
149+
Map<String, Object> variables = new HashMap<>();
150+
variables.put("item", "data");
151+
// Missing 'type' variable
152+
153+
PromptTemplate promptTemplate = PromptTemplate.builder()
154+
.template("Processing {item} with {type}")
155+
.variables(variables)
156+
.build();
157+
158+
try {
159+
promptTemplate.render();
160+
Assertions.fail("Expected IllegalStateException was not thrown.");
161+
}
162+
catch (IllegalStateException e) {
163+
assertThat(e.getMessage()).contains("Missing variable names are: [type]");
164+
}
165+
}
166+
167+
@Test
168+
void builderWithCompleteVariablesShouldRender() {
169+
Map<String, Object> variables = new HashMap<>();
170+
variables.put("item", "data");
171+
variables.put("count", 42);
172+
173+
PromptTemplate promptTemplate = PromptTemplate.builder()
174+
.template("Processing {item} with count {count}")
175+
.variables(variables)
176+
.build();
177+
178+
String result = promptTemplate.render();
179+
assertThat(result).isEqualTo("Processing data with count 42");
180+
}
181+
182+
@Test
183+
void builderWithEmptyStringVariableShouldWork() {
184+
Map<String, Object> variables = new HashMap<>();
185+
variables.put("name", "");
186+
187+
PromptTemplate promptTemplate = PromptTemplate.builder()
188+
.template("Hello '{name}'!")
189+
.variables(variables)
190+
.build();
191+
192+
String result = promptTemplate.render();
193+
assertThat(result).isEqualTo("Hello ''!");
194+
}
195+
99196
}

spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionBuilderTests.java

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
import static org.assertj.core.api.Assertions.assertThat;
2929
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.AND;
3030
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.EQ;
31+
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GT;
3132
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GTE;
3233
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.IN;
34+
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LT;
35+
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LTE;
3336
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NE;
3437
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NIN;
3538
import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NOT;
@@ -121,4 +124,122 @@ public void tesNot() {
121124
null));
122125
}
123126

127+
@Test
128+
public void testLessThanOperators() {
129+
// value < 1
130+
var ltExp = this.b.lt("value", 1).build();
131+
assertThat(ltExp).isEqualTo(new Expression(LT, new Key("value"), new Value(1)));
132+
133+
// value <= 1
134+
var lteExp = this.b.lte("value", 1).build();
135+
assertThat(lteExp).isEqualTo(new Expression(LTE, new Key("value"), new Value(1)));
136+
}
137+
138+
@Test
139+
public void testGreaterThanOperators() {
140+
// value > 1
141+
var gtExp = this.b.gt("value", 1).build();
142+
assertThat(gtExp).isEqualTo(new Expression(GT, new Key("value"), new Value(1)));
143+
144+
// value >= 10
145+
var gteExp = this.b.gte("value", 10).build();
146+
assertThat(gteExp).isEqualTo(new Expression(GTE, new Key("value"), new Value(10)));
147+
}
148+
149+
@Test
150+
public void testNullValues() {
151+
// status == null
152+
var exp = this.b.eq("status", null).build();
153+
assertThat(exp).isEqualTo(new Expression(EQ, new Key("status"), new Value(null)));
154+
}
155+
156+
@Test
157+
public void testEmptyInClause() {
158+
// category IN []
159+
var exp = this.b.in("category").build();
160+
assertThat(exp).isEqualTo(new Expression(IN, new Key("category"), new Value(List.of())));
161+
}
162+
163+
@Test
164+
public void testSingleValueInClause() {
165+
// type IN ["basic"]
166+
var exp = this.b.in("type", "basic").build();
167+
assertThat(exp).isEqualTo(new Expression(IN, new Key("type"), new Value(List.of("basic"))));
168+
}
169+
170+
@Test
171+
public void testComplexNestedGroups() {
172+
// ((level >= 1 AND level <= 5) OR status == "special") AND (region IN ["north",
173+
// "south"] OR enabled == true)
174+
var exp = this.b.and(
175+
this.b.or(this.b.group(this.b.and(this.b.gte("level", 1), this.b.lte("level", 5))),
176+
this.b.eq("status", "special")),
177+
this.b.group(this.b.or(this.b.in("region", "north", "south"), this.b.eq("enabled", true))))
178+
.build();
179+
180+
Expression expected = new Expression(AND,
181+
new Expression(OR,
182+
new Group(new Expression(AND, new Expression(GTE, new Key("level"), new Value(1)),
183+
new Expression(LTE, new Key("level"), new Value(5)))),
184+
new Expression(EQ, new Key("status"), new Value("special"))),
185+
new Group(
186+
new Expression(OR, new Expression(IN, new Key("region"), new Value(List.of("north", "south"))),
187+
new Expression(EQ, new Key("enabled"), new Value(true)))));
188+
189+
assertThat(exp).isEqualTo(expected);
190+
}
191+
192+
@Test
193+
public void testNotWithSimpleExpression() {
194+
// NOT (active == true)
195+
var exp = this.b.not(this.b.eq("active", true)).build();
196+
assertThat(exp).isEqualTo(new Expression(NOT, new Expression(EQ, new Key("active"), new Value(true)), null));
197+
}
198+
199+
@Test
200+
public void testNotWithGroup() {
201+
// NOT (level >= 3 AND region == "east")
202+
var exp = this.b.not(this.b.group(this.b.and(this.b.gte("level", 3), this.b.eq("region", "east")))).build();
203+
204+
Expression expected = new Expression(NOT,
205+
new Group(new Expression(AND, new Expression(GTE, new Key("level"), new Value(3)),
206+
new Expression(EQ, new Key("region"), new Value("east")))),
207+
null);
208+
209+
assertThat(exp).isEqualTo(expected);
210+
}
211+
212+
@Test
213+
public void testMultipleNotOperators() {
214+
// NOT (NOT (active == true))
215+
var exp = this.b.not(this.b.not(this.b.eq("active", true))).build();
216+
217+
Expression expected = new Expression(NOT,
218+
new Expression(NOT, new Expression(EQ, new Key("active"), new Value(true)), null), null);
219+
220+
assertThat(exp).isEqualTo(expected);
221+
}
222+
223+
@Test
224+
public void testSpecialCharactersInKeys() {
225+
// "item.name" == "test" AND "meta-data" != null
226+
var exp = this.b.and(this.b.eq("item.name", "test"), this.b.ne("meta-data", null)).build();
227+
228+
Expression expected = new Expression(AND, new Expression(EQ, new Key("item.name"), new Value("test")),
229+
new Expression(NE, new Key("meta-data"), new Value(null)));
230+
231+
assertThat(exp).isEqualTo(expected);
232+
}
233+
234+
@Test
235+
public void testEmptyStringValues() {
236+
// description == "" OR label != ""
237+
var exp = this.b.or(this.b.eq("description", ""), this.b.ne("label", "")).build();
238+
239+
Expression expected = new Expression(OR, new Expression(EQ, new Key("description"), new Value("")),
240+
new Expression(NE, new Key("label"), new Value("")));
241+
242+
assertThat(exp).isEqualTo(expected);
243+
}
244+
124245
}

0 commit comments

Comments
 (0)