2

We are building an application which will be using Kafka Streams. We are looking for sample example which shows us how to write a test case for a scenario, where we are calling an external service from Kafka topology. Basically that external call needs to be Mocked somehow, as service might not be running always. We are using TopologyTestDriver for writing test case. Due to this external call our test case is not executing. Getting error : org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:8080/endPointName": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect

Sample code for which we want to write test case:

@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}

public void method(StreamsBuilder builder) {
    builder.stream(inTopic,Consumed.with(StreamsSerdes.String(),new StreamsSerdes.CustomSerde()))
        .peek((s, customObj) -> LOG.info(customObj))
        .mapValues(this::getResult)
        .peek((s, result) -> LOG.info(result))
        .to(outputTopic,Produced.with(StreamsSerdes.String(),new ResultSerde()));
}

private Result getResult(Custom customObj) {
    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    HttpEntity<Custom> request = new HttpEntity<>(customObj, headers);
    return restTemplate.postForEntity(restCompleteUri, request, Result.class).getBody();
}

Sample Test Case Example:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest
public class TopologyTest {
    private static TopologyTestDriver topologyTestDriver;
    private static final Logger LOG = LogManager.getLogger();

@Autowired
private ConfigurableApplicationContext appContext;

@BeforeAll
void setUp() {
    Properties properties = getProperties();

    StreamsBuilder builder = new StreamsBuilder();
    appContext.getBean(PublisherSubscriberTopology.class).withBuilder(builder);
    Topology topology = builder.build();

    topologyTestDriver = new TopologyTestDriver(topology, properties);
}

private Properties getProperties() {
    Properties properties = new Properties();
    properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "test");
    properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "test:9092");
    properties.put(StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, LogDeserializationExceptionHandler.class.getName());
    properties.put(StreamsConfig.DEFAULT_PRODUCTION_EXCEPTION_HANDLER_CLASS_CONFIG, CustomProductionExceptionHandler.class.getName());
    return properties;
}

@Test
void testAppFlow() {
    Custom customObj = getCustomObj();
    Result result = getResult();

    ConsumerRecordFactory<String, Custom> resultFactory =
            new ConsumerRecordFactory<>(inTopic,
                    StreamsSerdes.String().serializer(), StreamsSerdes.CustomSerde().serializer());

    topologyTestDriver.pipeInput(resultFactory.create(
            inTopic,
            "1001",
            customObj
    ));

    ProducerRecord<String, Result> record =
            topologyTestDriver.readOutput(
                    outputTopic,
                    StreamsSerdes.String().deserializer(),
                    StreamsSerdes.ResultSerde().deserializer()
            );

    assertAll(() -> assertEquals("abc123", record.value().getABC()));
}

private Custom getCustomObj() {
    Custom customObj = new Custom();
    //setting customObj
    return customObj;
}

private Result getResult() {
    Result result = new Result();
    //setting resultObj
    return result;
}

@AfterAll
static void tearDown() {
    try {
        topologyTestDriver.close();
    } catch (Exception e) {
        LOG.error(e.getMessage());
    }
}

}

vivek075
  • 71
  • 6

2 Answers2

1

In this particular case Consider Refactoring of the existing code - abstract out the call to HTTP to some interface and mock it. Since you're using spring anyway, inject the bean that will work with HTTP, and instead of invoking

public void method(StreamsBuilder builder) {
    builder.stream(inTopic,Consumed.with(StreamsSerdes.String(),new StreamsSerdes.CustomSerde()))
        .peek((s, customObj) -> LOG.info(customObj))
        .mapValues(this::getResult)
        .peek((s, result) -> LOG.info(result))
        .to(outputTopic,Produced.with(StreamsSerdes.String(),new ResultSerde()));
}

private Result getResult(Custom customObj) {
   ... HTTP call here ...
}  

Use something like this:


class Whatever {
  @Autowired
  private HttpFacade httpFacade;

  public void method(StreamsBuilder builder) {...}

  private Result getResult(Custom customObj) {
      // httpFacade is int
      httpFacade.callRemoteService(customObj);
   }
}

@Component
class HttpFacade {
   public ABC callRemoteService(CustomObj) {
     ... here comes the code that works with HttpClient
   }
}

With this setup you can mock out the HttpFacade in the test (by using @MockBean or in plain mockito if you're running unit test without spring) And specify the expectations.

This is for you concrete case.

In general, if you have to test that the Http Request is populated with right URL, headers, body, etc. You can use WireMock.

For Kafka Streams, since it's a client library for Kafka, you can start up Kafka docker Test Container (and maybe Zookeeper) before the test, set it up to create the required topics and you're good to go.

This makes sense in case you want to test real kafka interaction and really want to make sure that the message gets to the kafka topic, then gets consumed by your consumer, etc, in other words, more complicated cases.

If you are using spring kafka, there is also an option to use Embedded Kafka, but I'm not really sure whether it will work with kafka streams - you should try, but at least its a "viable" direction

Update (based on op's comment):

When using mock bean in the spring driven test you will have to specify expectations on that bean:


@SpringBootTest // or whatever configuration you have to run spring driven test here
public class MyTest {

   @MockBean
   private HttpFacade httpFacade;

   @Test
   public void myTest() {
     Mockito.when(httpFacade).callRemoteService(eq(<YOUR_EXPECTED_CUSTOM_OBJECT_1>)).thenReturn(<EXPECTED_RESPONSE_1);
... its possible to specify as many expectations as you wish...

     ... run the test code here that you probably already have...
   }
}

The point is that you don't really need to make HTTP calls to test your kafka streams code!

Mark Bramnik
  • 39,963
  • 4
  • 57
  • 97
  • Can you please share/tell how the modified test case will become in this scenario(in my test class-TopologyTest). Basically how that Mock endpoint will be used in test case for TopologyTest? – vivek075 Jun 14 '21 at 08:41
  • @vivek075 - I've updated the answer, please read and let me know whether it suits your needs – Mark Bramnik Jun 14 '21 at 08:56
  • Thanks for answering. Problem still persists, as when the topology is executed that rest endpoint will be called, bottleneck lies there. How that mocked rest endpoint be triggered. If I don't make http call(mocked) then how kafka stream flow will be unit tested; As rest endpoint returns the result which will be pushed to output topic. – vivek075 Jun 14 '21 at 13:36
  • 1
    That's exactly the point of mock. Stub, in this case, to be precise if you care about the terminology. Its meant for specifying expectations. So you "simulate" the answers from remote HTTP server which are out of your test context anyway. You don't test that HTTP server works, you test that kafka streams code does what it needs to do based on the results of the stubbed HTTP call. – Mark Bramnik Jun 14 '21 at 15:33
  • How that stub will be injected basically when the topology is executed, won't it will execute actual rest endpoint? If you can show me how we can modify my @Test case to utilize that mock endpoint? – vivek075 Jun 17 '21 at 09:49
  • `HttpFacade` is a bean its driven by Spring. `@MockBean` will substitute the actual implementation with mock and will inject it in all the places its consumed automatically. This is done by spring testing framework. Then as long as you specify your expectations in the test, the actual kafka stream code will work against the mock automatically. You should really try it, probably even without the kafka streams, only to grasp the concept of `@MockBean` – Mark Bramnik Jun 17 '21 at 09:54
  • Thanks a lot for your response and enlightening my little knowledge. Your answer absolutely works. – vivek075 Jun 17 '21 at 11:33
  • Sure thing you’re welcome, since you’re new in SO, please make sure that you’ve accepted the answer - mine or the other one (whatever you feel works for you better) so that other people who will come across this question will consider it solved and won’t bother writing other answers…. – Mark Bramnik Jun 17 '21 at 11:35
  • Seems I need at least 15 reputation to cast a vote, although I have voted for the answer. – vivek075 Jun 17 '21 at 11:45
0

The problem is not related to Kafka Streams. You're depending on an HTTP Client in the middle of the topology (not recommended, by the way), so you need to be asking how you would test that.

You need to inject a mocked RestTemplate and restCompleteUri variable to communicate with some fake HTTP endpoint.

For example, WireMock or see these posts

OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
  • Can you please tell me then what is the recommended way. Basically it's a stateless Kafka stream; so we can't maintain anything in KTable. The rest endpoint we are calling is just a transformer, which will transform the input to output. Also I know we need to Mock rest endpoint but how to embed into kafka topology. – vivek075 Jun 09 '21 at 08:59
  • TopologyTestDriver should handle that for you and is unrelated to the Rest client, or being stateless/stateful, as I've answered. If it's "just a transform", then you should pull in the necessary code to do that completely in local JVM rather than use an external HTTP endpoint – OneCricketeer Jun 09 '21 at 11:11
  • TopologyTestDriver should handle that for you and is unrelated to the Rest client - How?? – vivek075 Jun 10 '21 at 09:20
  • The StreamsBuilder and RestTemplate are independent objects that are instantiated separately from one another. Your only requirement is both be non-null, but both can be mocked/faked – OneCricketeer Jun 10 '21 at 11:50
  • So should I assume its not achievable at the moment, or Kafka Streams does not provide enough functionality for writing unit test cases for these kind of scenarios? I am very new to Kafka Streams. Hence asking these questions. Apologies if you find something incorrect with my understanding. Do correct me if its feasible? – vivek075 Jun 11 '21 at 07:14
  • It's entirely possible. I told you, your problem is unrelated to Kafka Streams. You can use TestTopologyDriver with a mocked http client perfectly fine. Your question only has two methods, and no actual unit tests, so I don't know what you've tried or how your RestTemplate is defined to give you a more definitive answer – OneCricketeer Jun 11 '21 at 12:56
  • Added Sample Test case, now if you can help me? – vivek075 Jun 14 '21 at 03:23