ASSERTIONS
Fuzzy Matching
What You'll Learn
- Handle optional fields and dynamic values in responses
- Use contains operations for partial object and array matching
- Apply deep matching for nested structures
- Combine fuzzy markers for complex validation scenarios
- Validate arrays with flexible ordering requirements
- Master the not operator for negative assertions
Overview
Fuzzy matching extends Karate's validation capabilities to handle real-world scenarios where responses contain optional fields, dynamic values, or varying structures. These powerful operators make your tests more maintainable and resilient to changes.
Optional Fields
Double Hash Notation
The ##
prefix makes any field optional - it can be present with a valid value or completely absent:
# Schema with optional fields
* def userSchema = {
id: '#number',
name: '#string',
email: '##string', # Optional string
phone: '##string', # Optional string
age: '##number', # Optional number
metadata: '##object' # Optional object
}
# Both of these match the schema
* match { id: 1, name: 'John' } == userSchema
* match { id: 2, name: 'Jane', email: 'jane@example.com' } == userSchema
Null vs Not Present
Understanding the distinction between null and not present is crucial:
# Field is present but null
* def response1 = { name: 'John', age: null }
* match response1 == { name: '#string', age: '#null' }
# Field is not present at all
* def response2 = { name: 'John' }
* match response2 == { name: '#string', age: '#notpresent' }
# Field can be either null or not present
* match response1 == { name: '#string', age: '##null' }
* match response2 == { name: '#string', age: '##null' }
Contains Operations
Basic Contains
The contains
keyword checks for the presence of specific keys or values without requiring exact matches:
# Object contains
* def response = { id: 1, name: 'John', age: 30, city: 'NYC' }
* match response contains { name: 'John' }
* match response contains { name: 'John', age: 30 }
# Array contains
* def numbers = [1, 2, 3, 4, 5]
* match numbers contains 3
* match numbers contains [2, 4]
# String contains
* def message = 'Hello World!'
* match message contains 'World'
Contains Deep
For nested structures, contains deep
performs recursive matching:
Scenario: Deep contains for nested objects
* def response = {
user: {
id: 1,
profile: {
name: 'John',
settings: {
theme: 'dark',
notifications: {
email: true,
sms: false
}
}
}
}
}
# Deep match specific nested values
* match response contains deep {
user: {
profile: {
settings: {
notifications: {
email: true
}
}
}
}
}
Contains Only
When you need to ensure an array has exactly the specified elements (but order doesn't matter):
# Array must contain exactly these elements
* def data = { foo: [1, 2, 3] }
* match data.foo contains only [3, 2, 1]
* match data.foo contains only [2, 3, 1]
# This will fail - missing element
# * match data.foo contains only [2, 3]
# This will fail - extra element
# * match data.foo contains only [1, 2, 3, 4]
Contains Any
Check if at least one element from a set is present:
# Array contains any
* def numbers = [1, 2, 3]
* match numbers contains any [9, 2, 8] # Passes because 2 is present
# Object contains any
* def user = { name: 'John', age: 30 }
* match user contains any { age: 30, city: 'NYC' } # Passes because age matches
Contains Only Deep
Combines contains only
with deep matching for unordered array comparison at all levels:
# Arrays at any depth are compared without order
* def response = {
items: [
{ id: 1, tags: ['a', 'b'] },
{ id: 2, tags: ['c', 'd'] }
]
}
* match response contains only deep {
items: [
{ id: 2, tags: ['d', 'c'] }, # Different order
{ id: 1, tags: ['b', 'a'] } # Different order
]
}
Not Operator
Basic Not Contains
The !
operator negates contains assertions:
# Object not contains
* def user = { name: 'John', age: 30 }
* match user !contains { city: '#notnull' }
* match user !contains { age: 40 }
# Array not contains
* def numbers = [1, 2, 3]
* match numbers !contains 4
* match numbers !contains [5, 6]
# String not contains
* def message = 'Hello World!'
* match message !contains 'Goodbye'
Complex Negations
# Validate absence of multiple fields
* def response = { status: 'active', data: [] }
* match response !contains { error: '#notnull' }
* match response !contains { errorCode: '#present' }
# Ensure array doesn't contain specific patterns
* def users = [
{ name: 'John', role: 'user' },
{ name: 'Jane', role: 'user' }
]
* match users !contains { role: 'admin' }
Match Each with Fuzzy Matching
Each Contains
Combine match each
with contains
for flexible array validation:
# Each element contains certain fields
* def users = [
{ id: 1, name: 'John', age: 30, city: 'NYC' },
{ id: 2, name: 'Jane', age: 25, city: 'LA' }
]
* match each users contains { id: '#number', name: '#string' }
* match each users contains { age: '#? _ > 0' }
Each Contains Deep
For complex nested array structures:
Given def response = [
{
id: 1,
nested: {
data: {
value: 'A',
metadata: { created: '2024-01-01' }
}
}
},
{
id: 2,
nested: {
data: {
value: 'B',
metadata: { created: '2024-01-02' }
}
}
}
]
Then match each response contains deep {
nested: {
data: {
value: '#string'
}
}
}
Advanced Patterns
Conditional Presence
Validate fields based on conditions:
# If type is 'premium', certain fields must exist
* def validateUser = function(user) {
if (user.type === 'premium') {
return user.benefits != null && user.expiryDate != null;
}
return true;
}
* match each users == '#? validateUser(_)'
Dynamic Field Validation
Handle responses with variable structures:
# Response can have different shapes
* def response = { type: 'user', userData: { name: 'John' } }
* def schema = {
type: '#string',
userData: '##object',
productData: '##object',
errorData: '##object'
}
* match response == schema
# Validate based on type
* if (response.type == 'user') match response contains { userData: '#object' }
* if (response.type == 'product') match response contains { productData: '#object' }
* if (response.type == 'error') match response contains { errorData: '#object' }
Partial Array Matching
Match specific elements in arrays without checking all:
# Large array - only check specific positions
* def largeArray = karate.range(1, 100)
* match largeArray[0] == 1
* match largeArray[99] == 100
* match largeArray contains [1, 50, 100]
# Check array segments
* def data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
* match data[0:3] == [1, 2, 3]
* match data[-3:] == [8, 9, 10]
Real-World Examples
API Pagination Response
# Flexible pagination response validation
* def paginationSchema = {
data: '#[]',
meta: {
page: '#number',
pageSize: '#number',
total: '#number',
hasNext: '#boolean',
hasPrev: '#boolean'
},
links: '##object' # Optional links section
}
* match response == paginationSchema
* match response.data contains only '#object'
Search Results Validation
# Search results with optional fields
* def searchResultSchema = {
query: '#string',
count: '#number',
results: '#[]',
suggestions: '##[]', # Optional suggestions
corrections: '##[]', # Optional spell corrections
facets: '##object', # Optional facets
debug: '##object' # Optional debug info
}
* match response == searchResultSchema
# Validate each result has required fields
* match each response.results contains {
id: '#string',
title: '#string',
score: '#number'
}
Error Response Handling
# Flexible error response validation
* def errorSchema = {
error: '#string',
code: '#string',
message: '#string',
details: '##[]', # Optional detail array
timestamp: '##string', # Optional timestamp
requestId: '##uuid', # Optional request ID
help: '##string' # Optional help URL
}
# Success response shouldn't have error fields
* if (responseStatus == 200) match response !contains { error: '#notnull' }
# Error response should match schema
* if (responseStatus >= 400) match response == errorSchema
Best Practices
Schema Design
# ✅ Good: Use optional fields appropriately
* def flexibleSchema = {
required: '#string',
optional: '##string',
conditionallyRequired: '#? someCondition ? _ != null : true'
}
# ❌ Avoid: Making everything optional
* def tooFlexible = {
everything: '##string',
isOptional: '##number'
}
Contains Usage
# ✅ Good: Use contains for known subsets
* match response contains { status: 'success' }
# ✅ Good: Use contains deep for nested validation
* match response contains deep { user: { active: true } }
# ❌ Avoid: Using contains when exact match is needed
* match response contains { id: 1 } # Should use == if id is the only field
Performance Considerations
# ✅ Good: Validate only what's necessary
* match response.criticalFields == { /* only critical validations */ }
# ✅ Good: Use early termination
* if (response.error) karate.fail('Error: ' + response.error)
# ❌ Avoid: Deep validation on large arrays unnecessarily
* match each response.thousandItems contains deep { /* complex validation */ }
Troubleshooting
Common Issues
# Issue: Optional field validation fails
# Solution: Use ## prefix
* def schema = { optional: '##string' } # Not just '#string'
# Issue: Deep contains not working
# Solution: Ensure 'deep' keyword is present
* match response contains deep { nested: { field: 'value' } }
# Issue: Array order matters unexpectedly
# Solution: Use 'contains only' or 'contains'
* match array contains only [3, 2, 1] # Order doesn't matter
Debugging Fuzzy Matches
# Print actual vs expected for debugging
* print 'Actual:', response
* print 'Checking contains:', { expectedField: 'value' }
* match response contains { expectedField: 'value' }
# Break down complex validations
* match response.user == '#object'
* match response.user.profile == '#object'
* match response.user.profile.settings contains { theme: 'dark' }
Next Steps
- Learn about Reusability for test organization
- Explore Advanced Features for complex scenarios
- Understand Extensions for custom validation