ว่างๆ ผมคิดดูว่าถ้าเราจะทำ end-to-end testing กับระบบปัจจุบันของเราที่ประกอบไปด้วย API และ Kafka น่าจะมี tool ให้เลือกใช้อยู่บ้าง งั้นลองใช้ Cucumber ซึ่งเป็น behavior-driven-development (BDD) ดู น่าจะช่วยทำให้ development team ทำงานด้วยกันได้ง่ายขึ้น เนื่องจากภาษาที่ใช้เขียนเป็น Given-When-Then แบบคนธรรมดาเข้าใจได้เลย

Trigger result

https://www.melvinvivas.com/developing-microservices-using-kafka-and-mongodb/

Cucumber มีส่วนประกอบคร่าวๆ อยู่ 2 ส่วน

  • Feature ไว้กำหนด scenario เขียนโดยใช้ Gherkin syntax เช่น กำหนดตัวแปรของ test หรือจัดกลุ่มโดยใช้ @tags
  • Step definitions เป็น code ที่ใช้เขียนตาม Scenario ที่เราได้กำหนดไว้ (ในตัวอย่างจะใช้ Java)

เริ่มจากสร้าง Scenario มาซักเคสนึงก่อน

Feature: Create an Order
Scenario: Happy path
Given a order request as described in "order.json"
When send a request to create an order successfully
And wait for notification from the system within 5 seconds
Then a user should receive a notification with a correct id
{
"soldTo": "E2E",
"shipTo": "E2E",
"items": [
{
"name": "Diesel",
"price": 2000,
"currency": "THB"
}
]
}
view raw order.json hosted with ❤ by GitHub

ระวัง ถ้าตัวแปรของเราเป็น String ต้องมี double-quote ด้วยนะ ไม่งั้น Cucumber มันนึกว่าเป็น description เฉยๆ

มาถึงในส่วนของ coding ละ

เริ่มจาก dependencies เหมือนเคย

ทำตาม https://cucumber.io/docs/tools/java/#gradle ได้เลยสำหรับคนใช้ Gradle

plugins {
id 'java'
}
group 'com.raksit.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
// ตาม documentation
configurations {
cucumberRuntime {
extendsFrom testImplementation
}
}
dependencies {
testImplementation 'io.cucumber:cucumber-java:5.6.0' // ลง Hamcrest มาละ หรือจะลง JUnit เองก็ได้ แล้วต่
testImplementation 'io.rest-assured:rest-assured:3.3.0' // สำหรับ API testing
testImplementation 'org.apache.kafka:kafka-clients:2.5.0'
testImplementation 'commons-io:commons-io:2.7' // สำหรับอ่าน JSON กับ property file
}
// เปลี่ยน package ตาม group id ที่ set ไว้ข้างบนได้เลย
task cucumber() {
dependsOn assemble, compileTestJava
doLast {
javaexec {
main = "io.cucumber.core.cli.Main"
classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output
args = ['--plugin', 'pretty', '--glue', 'com/raksit/example', 'src/test/resources']
// ขี้เกียจทำ configuration file อ่านจาก System property แบบขำๆ ไป ฮ่าๆๆ
systemProperty "orderHostName", System.getProperty("orderHostName")
systemProperty "kafkaBootstrapServers", System.getProperty("kafkaBootstrapServers")
systemProperty "token", System.getProperty("token")
}
}
}
view raw build.gradle hosted with ❤ by GitHub

เอา Scenario file พร้อมกับ JSON file ไปไว้ใน src/test/resources

src
│   build.gradle
└───test
│   │
│   └───your.package.name // สำหรับเก็บ step definitions
│       |   
|       └───YourStepDefinitions.java
│   |   
|   └───resources // สำหรับเก็บ feature
│       |   
|       └───YourScenario.feature
│       |   
|       └───your.json
│
│       

สร้าง class เอาไว้ใช้จัดการกับ Kafka

ปัญหาคือเราทำการทดสอบกับระบบจริงๆ ทีนี้ตอนเรา subscribe message มาเราจะรู้ได้ไงว่าอันไหนเป็นของเรา วิธีของผมง่ายๆคือ ก็ให้ Kafka client ทำการ poll หรือ subscribe และดึง message ออกมาเรื่อยๆ ทุกๆ วินาทีก็ได้ แล้วค่อยเอา message มาเทียบหาเอา เช่น id เป็นต้น

สร้าง kafka.properties ไว้ใน src/test/resources

group.id=test
acks=all
buffer.memory=2097152
batch.size=1048576
compression.type=lz4
key.deserializer=org.apache.kafka.common.serialization.IntegerDeserializer
value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
auto.offset.reset=earliest

public class NotificationReceiver {
private final String topic;
public NotificationReceiver(String topic) {
this.topic = topic;
}
public List<String> poll(int seconds) throws IOException {
List<String> records = new ArrayList<>();
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(getProperties());
kafkaConsumer.subscribe(Collections.singletonList(topic));
try {
long maxAllowedLatency = seconds * 1000;
long endPollingTimestamp = System.currentTimeMillis() + maxAllowedLatency;
while ( System.currentTimeMillis() < endPollingTimestamp ) {
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.of(1, ChronoUnit.SECONDS));
for ( ConsumerRecord<String, String> next : consumerRecords ) {
System.out.println(next.value());
records.add(next.value());
}
}
} finally {
kafkaConsumer.close();
}
return records;
}
private Properties getProperties() throws IOException {
FileReader reader = new FileReader(FileUtils.getFile("src", "test", "resources", "kafka.properties"));
Properties properties = new Properties();
properties.load(reader);
properties.setProperty("bootstrap.servers", System.getProperty("kafkaBootstrapServers"));
return properties;
}
}

สร้าง Step definitions

public class CreateAnOrderStepDefinitions {
private String orderRequestBody;
private String createdOrderId;
private List<String> records;
@Given("a order request as described in {string}")
public void readAnOrderRequestFromJsonFile(String jsonPath) throws IOException {
File file = FileUtils.getFile("src", "test", "resources", jsonPath);
orderRequestBody = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
}
@When("send a request to create an order successfully")
public void createAndOrder() {
Response response = given()
.contentType(ContentType.JSON)
.body(orderRequestBody)
.when()
.header("Authorization",
"Bearer " + System.getProperty("token"))
.post(System.getProperty("orderHostName") + "/orders")
.then()
.statusCode(HttpStatus.SC_CREATED)
.extract()
.response();
createdOrderId = response.jsonPath().get("id");
}
@When("wait for notification from the system within {int} seconds")
public void waitForSystemNotification(int waitingSeconds) throws IOException {
records = new NotificationReceiver("order.created").poll(waitingSeconds);
}
@Then("a user should receive a notification with a correct id")
public void shouldReceivedNotificationWithCorrectId() {
assertThat(records.stream().anyMatch(record -> record.contains(createdOrderId)), equalTo(true));
}
}

ปิดท้ายด้วยการ run ผ่าน Gradle CLI

./gradlew cucumber -Dtoken=<your-token-here> -DorderHostName=<your-api-hostname> -DkafkaBootstrapServers=<your-kafka-here>

ได้ผลลัพธ์หน้าตาประมาณนี้

Scenario: Happy path                                          # src/test/resources/CreateAnOrder.feature:2
  Given a order request as described in "order.json"          # com.raksit.example.CreateAnOrderStepDefinitions.readAnOrderRequestFromJsonFile(java.lang.String)
  When send a request to create an order successfully         # com.raksit.example.CreateAnOrderStepDefinitions.createAndOrder()

{"orderId":"c62c5eb4-62b4-48dc-bf55-64629dc800a6"}
  And wait for notification from the system within 5 seconds  # com.raksit.example.CreateAnOrderStepDefinitions.waitForSystemNotification(int)
  Then a user should receive a notification with a correct id # com.raksit.example.CreateAnOrderStepDefinitions.shouldReceivedNotificationWithCorrectId()

1 Scenarios (1 passed)
4 Steps (4 passed)
0m6.729s

Trigger result

code ตัวอย่าง https://github.com/raksit31667/example-cucumber-restassured-kafka