Skip to main content

Overview

Validation classes provide high-level, convenient methods for interacting with services in tests. They wrap around the service client and account, abstracting away the complexity of the underlying APIs.

The Validation interface

The Validation interface is a marker interface with no required methods:
public interface Validation {
}
While the interface is empty, validation implementations provide service-specific testing utilities that make test code more readable and maintainable.

Why use validation classes?

Validation classes serve several important purposes:

Simplify client APIs

Wrap complex client APIs with simple, test-focused methods

Reduce boilerplate

Eliminate repetitive setup and teardown code in tests

Improve readability

Make test code more expressive and easier to understand

Encapsulate best practices

Encode testing patterns and conventions into reusable methods

Example: KafkaValidation

Let’s examine a real validation class from the Kafka service.

Class structure

From KafkaValidation.java:23-31:
public class KafkaValidation<T> implements Validation {
    private static final Logger LOG = LoggerFactory.getLogger(KafkaValidation.class);
    private final KafkaProducer<String, T> producer;
    private final KafkaConsumer<String, T> consumer;
    
    public KafkaValidation(KafkaProducer<String, T> producer, KafkaConsumer<String, T> consumer) {
        this.producer = producer;
        this.consumer = consumer;
    }
    
    // Methods...
}
The validation class stores references to the Kafka producer and consumer clients, which are injected during construction.

Producing messages

The validation class provides multiple overloaded produce() methods:
public void produce(String topic, T message) {
    produce(topic, message, Collections.emptyList());
}
Usage:
kafka.validation().produce("myTopic", "Hello Kafka!");

Consuming messages

From KafkaValidation.java:86-90:
public List<ConsumerRecord<String, T>> consume(String topic) {
    consumer.subscribe(Collections.singletonList(topic));
    consumer.seekToBeginning(consumer.assignment());
    return StreamSupport.stream(consumer.poll(Duration.ofSeconds(30)).records(topic).spliterator(), false)
        .collect(Collectors.toList());
}
Usage:
List<ConsumerRecord<String, String>> records = kafka.validation().consume("myTopic");
Assertions.assertEquals(1, records.size());
Assertions.assertEquals("Hello Kafka!", records.get(0).value());
The consume() method automatically subscribes to the topic, seeks to the beginning, and polls for 30 seconds.

Resource cleanup

public void closeProducer() {
    producer.close();
}

public void closeConsumer() {
    consumer.close();
}
These methods are called automatically by the service’s closeResources() method.

Creating validation instances

Validation instances are created in the service’s beforeAll() lifecycle method.

Example from Kafka service

From Kafka.java:36-58:
public <T> KafkaValidation<T> validation(Class<T> clazz) {
    if (!validations.containsKey(clazz)) {
        validations.put(clazz, createValidation(clazz));
    }
    return validations.get(clazz);
}

public KafkaValidation<String> validation() {
    return validation(String.class);
}

private <T> KafkaValidation<T> createValidation(Class<T> clazz) {
    if (clazz.isInstance(new byte[0])) {
        props.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName());
        props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName());
    } else if (clazz.isInstance("")) {
        props.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
    } else {
        throw new IllegalArgumentException("Unsupported class type passed to validation() method: " + clazz.getName());
    }
    return new KafkaValidation<>(new KafkaProducer<>(props), new KafkaConsumer<>(props));
}
Kafka’s validation is generic and supports multiple value types (String, byte[]). The service caches validation instances by type.

Using validation in tests

Here’s a complete test example showing the validation class in action:
public class KafkaTest {
    @RegisterExtension
    public static Kafka kafka = ServiceFactory.create(Kafka.class);
    
    @Test
    public void testProduceAndConsume() {
        final String topic = "myTopic";
        final String message = "Hello kafka!";
        
        // Produce a message
        kafka.validation().produce(topic, message);
        
        // Consume and verify
        final List<ConsumerRecord<String, String>> records = kafka.validation().consume(topic);
        Assertions.assertEquals(1, records.size());
        Assertions.assertEquals(message, records.get(0).value());
    }
    
    @Test
    public void testProduceWithHeaders() {
        final String topic = "headersTopic";
        final String message = "Message with headers";
        
        Map<String, String> headers = Map.of(
            "correlationId", "12345",
            "source", "integration-test"
        );
        
        // Produce with headers
        kafka.validation().produce(topic, message, headers);
        
        // Consume and verify headers
        final List<ConsumerRecord<String, String>> records = kafka.validation().consume(topic);
        Assertions.assertEquals(1, records.size());
        
        ConsumerRecord<String, String> record = records.get(0);
        Assertions.assertEquals(message, record.value());
        
        Header correlationId = record.headers().lastHeader("correlationId");
        Assertions.assertEquals("12345", new String(correlationId.value()));
    }
}

Validation design patterns

When implementing your own validation classes, follow these patterns:
1

Store client references

Accept client and account instances in the constructor and store them as fields.
public class MyServiceValidation implements Validation {
    private final MyServiceClient client;
    private final MyServiceAccount account;
    
    public MyServiceValidation(MyServiceClient client, MyServiceAccount account) {
        this.client = client;
        this.account = account;
    }
}
2

Provide test-focused methods

Create methods that address common testing scenarios, not just wrappers around client methods.
// Good: Test-focused method
public void createUserAndVerify(String username, String email) {
    User user = client.createUser(username, email);
    assertNotNull(user.getId());
    assertEquals(username, getUserFromAPI(user.getId()).getUsername());
}

// Less ideal: Simple wrapper
public User createUser(String username, String email) {
    return client.createUser(username, email);
}
3

Include logging

Add debug logging to help with troubleshooting test failures.
public void produce(String topic, T message) {
    LOG.debug("Producing message \"{}\" to topic \"{}\"", message, topic);
    producer.send(new ProducerRecord<>(topic, message));
}
4

Handle common edge cases

Build in retry logic, timeouts, and error handling appropriate for testing.
public List<Message> waitForMessages(String queue, int expectedCount) {
    return WaitUtils.waitFor(
        () -> client.getMessages(queue),
        messages -> messages.size() >= expectedCount,
        "Waiting for " + expectedCount + " messages in queue " + queue
    );
}
5

Evolve with usage

Continuously update validation classes as you discover new testing patterns.
Validation classes should grow organically based on actual test needs, not anticipated future requirements.

Best practices

Next steps

Services

Learn about the Service abstraction

Accounts

Understand account management

Deployment modes

Explore deployment options

Build docs developers (and LLMs) love