ทำ E2E testing กับ RESTful และ Kafka ด้วย Cucumber
ว่างๆ ผมคิดดูว่าถ้าเราจะทำ end-to-end testing กับระบบปัจจุบันของเราที่ประกอบไปด้วย API และ Kafka น่าจะมี tool ให้เลือกใช้อยู่บ้าง งั้นลองใช้ Cucumber ซึ่งเป็น behavior-driven-development (BDD) ดู น่าจะช่วยทำให้ development team ทำงานด้วยกันได้ง่ายขึ้น เนื่องจากภาษาที่ใช้เขียนเป็น Given-When-Then แบบคนธรรมดาเข้าใจได้เลย
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" | |
} | |
] | |
} |
ระวัง ถ้าตัวแปรของเราเป็น 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") | |
} | |
} | |
} |
เอา 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
code ตัวอย่าง https://github.com/raksit31667/example-cucumber-restassured-kafka