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!" );
public void produce ( String topic, Integer partition, Long timestamp, T message, List < Header > headers) {
// ...
producer . send ( new ProducerRecord (topic, partition, timestamp, Long . toString (timestamp), message, headers),
new Callback () {
public void onCompletion ( RecordMetadata recordMetadata , Exception e ) {
if (e == null ) {
LOG . debug ( "Received new metadata. \n "
+ "Topic:" + recordMetadata . topic () + " \n "
+ "Partition: " + recordMetadata . partition () + " \n "
+ "Offset: " + recordMetadata . offset () + " \n "
+ "Timestamp: " + recordMetadata . timestamp ());
} else {
LOG . debug ( "Error while producing" , e);
}
}
});
}
Usage: kafka . validation (). produce ( "myTopic" , 0 , System . currentTimeMillis (), "Hello Kafka!" , null );
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:
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;
}
}
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);
}
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));
}
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
);
}
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