# XML Assertions

This comprehensive guide explains how to write XML assertions for testing XML responses in Kusho using the new xmlAssert API.

NOTE: This is available only on our Enterprise plan for select users. If you're interested in enabling this, please reach out to us at enterprise@kusho.ai or your RM.

# Overview

Kusho now supports dual assertion systems:

  1. Traditional Chai assertions - For JSON responses (existing functionality, unchanged)
  2. New xmlAssert functions - For XML responses with XPath support

The system automatically detects assertion type and routes to appropriate handlers, ensuring 100% backward compatibility.

# Assertion Processing Model

Important: Assertions are processed line by line. Each line is treated as a separate, independent assertion.

# Assertion Type Detection

  • xmlAssert assertions: Lines starting with xmlAssert. - routed to XML-specific handler
  • Chai assertions: All other lines - routed to traditional Chai handler (unchanged)

# Line-by-Line Processing

// Each line below is processed as a separate assertion:
expect(response.status).to.equal(200)                           // Line 1: Chai assertion
xmlAssert.assertXPath(response.response, '//catalog', null, 'exists')  // Line 2: XML assertion
expect(response.headers['content-type']).to.include('xml')      // Line 3: Chai assertion
xmlAssert.assertXPath(response.response, '//product', 2, 'count')      // Line 4: XML assertion

# XML Assertion API

# Core Function

# xmlAssert.assertXPath(xmlString, xpathExpression, expectedValue, assertionType)

Purpose: Validate XPath results with automatic error throwing Parameters:

  • xmlString: XML content (usually response.response)
  • xpathExpression: XPath query string
  • expectedValue: Expected result (or null for existence checks)
  • assertionType: Type of assertion ('exists', 'count', 'text', 'value')

# Assertion Types

# 1. Existence Assertions ('exists')

Purpose: Verify that elements exist in XML

// Check if catalog element exists
xmlAssert.assertXPath(response.response, '//catalog', null, 'exists')

// Check if products with specific attributes exist
xmlAssert.assertXPath(response.response, '//product[@category="electronics"]', null, 'exists')

// Check if nested elements exist
xmlAssert.assertXPath(response.response, '//product/specifications/weight', null, 'exists')

# 2. Count Assertions ('count')

Purpose: Validate exact number of matching elements

// Verify total product count
xmlAssert.assertXPath(response.response, '//product', 5, 'count')

// Count products in specific category
xmlAssert.assertXPath(response.response, '//product[@category="electronics"]', 3, 'count')

// Count elements with specific text content
xmlAssert.assertXPath(response.response, '//product[price > 1000]', 2, 'count')

# 3. Text Content Assertions ('text')

Purpose: Validate text content of elements

// Check product name
xmlAssert.assertXPath(response.response, '//product[@id="1"]/name', 'iPhone 14', 'text')

// Verify category text
xmlAssert.assertXPath(response.response, '//product[1]/@category', 'electronics', 'text')

// Check nested text content
xmlAssert.assertXPath(response.response, '//catalog/description', 'Electronics Catalog', 'text')

# 4. Value Assertions ('value')

Purpose: Validate element values (supports both string and numeric comparisons)

// String value assertions
xmlAssert.assertXPath(response.response, '//product/status', 'active', 'value')

// Numeric value assertions
xmlAssert.assertXPath(response.response, '//product/price', 999.99, 'value')
xmlAssert.assertXPath(response.response, '//product/stock', 25, 'value')

// ID value assertions
xmlAssert.assertXPath(response.response, '//product/@id', '12345', 'value')

# Advanced XPath Patterns

# Element Selection

// Select by tag name
xmlAssert.assertXPath(response.response, '//product', null, 'exists')

// Select by attribute
xmlAssert.assertXPath(response.response, '//product[@id="123"]', null, 'exists')

// Select by position
xmlAssert.assertXPath(response.response, '//product[1]', null, 'exists')      // First product
xmlAssert.assertXPath(response.response, '//product[last()]', null, 'exists') // Last product

// Select by text content
xmlAssert.assertXPath(response.response, '//product[name="iPhone 14"]', null, 'exists')

# Attribute Queries

// Check attribute existence
xmlAssert.assertXPath(response.response, '//product[@category]', null, 'exists')

// Check attribute values
xmlAssert.assertXPath(response.response, '//product/@category', 'electronics', 'text')
xmlAssert.assertXPath(response.response, '//product[@id="1"]/@category', 'electronics', 'value')

// Count elements with specific attributes
xmlAssert.assertXPath(response.response, '//product[@available="true"]', 3, 'count')

# Conditional Expressions

// Elements with numeric conditions
xmlAssert.assertXPath(response.response, '//product[price > 500]', 4, 'count')
xmlAssert.assertXPath(response.response, '//product[stock < 10]', 2, 'count')

// Multiple conditions (AND)
xmlAssert.assertXPath(response.response, '//product[@category="electronics" and price > 1000]', 1, 'count')

// Text contains conditions
xmlAssert.assertXPath(response.response, '//product[contains(name, "iPhone")]', 3, 'count')

// Position-based conditions
xmlAssert.assertXPath(response.response, '//product[position() <= 3]', 3, 'count')

# Hierarchical Navigation

// Parent-child relationships
xmlAssert.assertXPath(response.response, '//catalog/products/product', 5, 'count')

// Descendant selection
xmlAssert.assertXPath(response.response, '//catalog//product', 5, 'count')

// Following sibling
xmlAssert.assertXPath(response.response, '//product[1]/following-sibling::product', 4, 'count')

// Ancestor selection
xmlAssert.assertXPath(response.response, '//name/ancestor::product', null, 'exists')

# Complex Query Examples

# E-commerce XML Response

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
    <metadata>
        <total>5</total>
        <category>Electronics</category>
    </metadata>
    <products>
        <product id="1" category="electronics" available="true">
            <name>iPhone 14</name>
            <price currency="USD">999.99</price>
            <stock>25</stock>
            <specifications>
                <weight unit="g">172</weight>
                <color>Blue</color>
            </specifications>
        </product>
        <product id="2" category="electronics" available="true">
            <name>MacBook Pro</name>
            <price currency="USD">1999.99</price>
            <stock>10</stock>
            <specifications>
                <weight unit="kg">1.4</weight>
                <color>Silver</color>
            </specifications>
        </product>
    </products>
</catalog>

# Comprehensive Test Suite

Note: Each line below is processed as an independent assertion:

// Basic structure validation - each line is a separate assertion
xmlAssert.assertXPath(response.response, '//catalog', null, 'exists')
xmlAssert.assertXPath(response.response, '//products', null, 'exists')
xmlAssert.assertXPath(response.response, '//metadata', null, 'exists')

// Count validations - independent line-by-line processing
xmlAssert.assertXPath(response.response, '//product', 2, 'count')
xmlAssert.assertXPath(response.response, '//product[@available="true"]', 2, 'count')

// Metadata assertions - each line evaluated separately
xmlAssert.assertXPath(response.response, '//metadata/total', '5', 'text')
xmlAssert.assertXPath(response.response, '//metadata/category', 'Electronics', 'text')

// Product-specific assertions - line-by-line evaluation
xmlAssert.assertXPath(response.response, '//product[@id="1"]/name', 'iPhone 14', 'text')
xmlAssert.assertXPath(response.response, '//product[@id="1"]/price', 999.99, 'value')
xmlAssert.assertXPath(response.response, '//product[@id="2"]/stock', 10, 'value')

// Attribute validations - independent assertions
xmlAssert.assertXPath(response.response, '//product[@id="1"]/@category', 'electronics', 'value')
xmlAssert.assertXPath(response.response, '//price/@currency', 'USD', 'text')

// Nested element assertions - each line is separate
xmlAssert.assertXPath(response.response, '//product[@id="1"]/specifications/weight', '172', 'text')
xmlAssert.assertXPath(response.response, '//specifications/color', 'Blue', 'text')

// Complex queries - single-line XPath expressions
xmlAssert.assertXPath(response.response, '//product[price > 1500]', 1, 'count')
xmlAssert.assertXPath(response.response, '//product[contains(name, "iPhone")]', 1, 'count')
xmlAssert.assertXPath(response.response, '//product[specifications/weight/@unit="kg"]', 1, 'count')

# Advanced Usage Patterns

# Single-Line Assertions Only

Important: Each assertion must be a complete statement on one line. Multi-line logic is not supported.

// ✅ CORRECT - Each line is a complete assertion
xmlAssert.assertXPath(response.response, '//product', 2, 'count')
xmlAssert.assertXPath(response.response, '//product[@id="1"]/name', 'iPhone 14', 'text')
expect(response.status).to.equal(200)

// ❌ INCORRECT - Multi-line logic not supported
const result = xmlAssert.xpath(response.response, '//product/price')
if (result.exists) {
    // This won't work as expected
}

// ❌ INCORRECT - Variable assignments across lines not supported
const productCount = xmlAssert.xpath(response.response, '//product').count
xmlAssert.assertXPath(response.response, '//metadata/total', productCount, 'value')

# Working with Single Lines

// ✅ CORRECT - Direct assertions on single lines
xmlAssert.assertXPath(response.response, '//product[price > 500]', 3, 'count')
xmlAssert.assertXPath(response.response, '//product[contains(name, "iPhone")]', 1, 'count')

// ✅ CORRECT - Use XPath expressions for complex logic instead of JavaScript
xmlAssert.assertXPath(response.response, '//product[specifications/weight/@unit="kg"]', 1, 'count')
xmlAssert.assertXPath(response.response, '//product[@category="electronics" and price > 1000]', 1, 'count')

# Error Handling

Important: Multi-line error handling logic is not supported due to line-by-line processing. Use single-line assertions only.

// ✅ CORRECT - Each assertion is independent and self-contained
xmlAssert.assertXPath(response.response, '//product', 2, 'count')
xmlAssert.assertXPath(response.response, '//required-field', null, 'exists')

// ❌ INCORRECT - Try-catch blocks don't work across line processing
try {
    xmlAssert.assertXPath(response.response, '//product', 5, 'count')  // Won't work as expected
} catch (error) {
    // This logic won't execute properly
}

// ❌ INCORRECT - Conditional logic across lines not supported
const result = xmlAssert.xpath(response.response, '//optional-field')
if (result.exists) {
    // This won't work due to line-by-line processing
}

// ✅ CORRECT - Use XPath conditional expressions instead
xmlAssert.assertXPath(response.response, '//element[condition]', expectedValue, 'assertion_type')

# Backward Compatibility

# Existing Chai Assertions (Unchanged)

Your existing JSON assertions continue to work exactly as before:

// These work unchanged - routed to traditional Chai handler
expect(response.status).to.equal(200)
expect(response.data).to.have.property('products')
expect(response.data.products).to.have.length(5)
expect(response.data.products[0]).to.have.property('name')
expect(response.data.products[0].name).to.equal('iPhone 14')

# Mixed Assertion Types

You can use both assertion types in the same test case. Each line is processed independently:

// Each line below is processed as a separate assertion
expect(response.status).to.equal(200)                                    // Line 1: Chai
expect(response.headers['content-type']).to.include('xml')               // Line 2: Chai
xmlAssert.assertXPath(response.response, '//catalog', null, 'exists')    // Line 3: xmlAssert
xmlAssert.assertXPath(response.response, '//product', 2, 'count')        // Line 4: xmlAssert
expect(response.data).to.have.property('timestamp')                      // Line 5: Chai

# Best Practices

# 1. Use Specific XPath Expressions

// Good - specific and targeted
xmlAssert.assertXPath(response.response, '//product[@id="1"]/name', 'iPhone 14', 'text')

// Avoid - too broad and potentially fragile
xmlAssert.assertXPath(response.response, '//*[text()="iPhone 14"]', null, 'exists')

# 2. Test Structure Before Content

// First verify structure exists
xmlAssert.assertXPath(response.response, '//products', null, 'exists')
xmlAssert.assertXPath(response.response, '//product', 5, 'count')

// Then test specific content
xmlAssert.assertXPath(response.response, '//product[1]/name', 'iPhone 14', 'text')

# 3. Handle Edge Cases

// Check for empty results
const result = xmlAssert.xpath(response.response, '//optional-element')
if (result.count === 0) {
    console.log('Optional element not present - this is acceptable')
} else {
    xmlAssert.assertXPath(response.response, '//optional-element', 'expected-value', 'text')
}

# 4. Use Appropriate Assertion Types

// Use 'exists' for presence checks
xmlAssert.assertXPath(response.response, '//required-field', null, 'exists')

// Use 'count' for quantity validation
xmlAssert.assertXPath(response.response, '//item', 10, 'count')

// Use 'text' for exact text matching
xmlAssert.assertXPath(response.response, '//status', 'active', 'text')

// Use 'value' for numeric or flexible comparisons
xmlAssert.assertXPath(response.response, '//price', 99.99, 'value')

# Troubleshooting

# Common Issues

1. XPath Expression Errors

  • Verify XPath syntax using browser dev tools
  • Test expressions in XML validators
  • Use browser's document.evaluate() to debug

2. Namespace Issues

  • XML with namespaces requires namespace-aware XPath
  • Consider using local-name() for namespace-agnostic queries

3. Text vs Value Confusion

  • Use 'text' for element text content
  • Use 'value' for attribute values and numeric comparisons

4. Case Sensitivity

  • XML is case-sensitive
  • Element names, attribute names, and values must match exactly

# Error Messages

The system provides descriptive error messages:

  • XPath '//product' should exist but no matching nodes found
  • XPath '//product' should match 5 nodes but got 3
  • XPath '//product/name' should have text 'iPhone 14' but got 'iPhone 13'

# Limitations

  1. XPath 1.0 Only: Browser limitations restrict to XPath 1.0 syntax
  2. Case Sensitive: All XML elements and attributes are case-sensitive
  3. First Node Priority: Text and value extractions use first matching node only
  4. No Namespace Prefixes: Complex namespace handling not supported by default

# Migration Guide

To add XML assertions to existing test cases:

  1. Keep existing assertions unchanged - they continue to work
  2. Add xmlAssert assertions for XML-specific validations
  3. Start with existence checks, then add specific validations
  4. Test thoroughly to ensure both assertion types work together

This comprehensive system ensures robust XML testing capabilities while maintaining complete backward compatibility with existing Chai-based assertions.