What Is Declarative Testing
Here is some similar insight from IEEE (opens in a new tab)-
We propose a software testing paradigm called declarative testing. In declarative testing, a test scenario focuses on what to accomplish rather than on the imperative details of how to manipulate the state of an application under test and verify the final application state against an expected state. Declarative testing is a test design paradigm which separates test automation code into conceptual Answer, Executor, and Verifier entities.
- According to Wikipedia (opens in a new tab) -
In computer science, declarative programming is a programming paradigm—a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow.
Advantages of Declarative Testing
- According to IEEE (opens in a new tab) -
Preliminary experience with declarative testing suggests that the modular characteristics of the paradigm may significantly enhance the ability of a testing effort to keep pace with the evolution of a software application during the application's development process.
-
Instead of writing code
howto achieve the testing goals, we writewhatto achieve in the test intentions i.e. the test input and the expectations -
Here the framework, behind the scene, handles the execution via necessary code to do the job for us e.g. API calls, DB executions, producing/consuming from Kafka topics etc
-
In this style we attempt to minimize or eliminate side effects by describing what the
testmust accomplish in terms of the business functionality, rather than describe how to accomplish it via programming or coding
That makes the test automation a lot easy and clean.
In the Declarative Style we don't need to write any of the below.
- The Http Client (or Kafka Client) calls for REST APIs
- Request payload parsing
- Response payload parsing
- Code for assertions/verifications e.g. comparing actual vs expected response
| Declarative Style | Traditional Style |
|---|---|
"url":"/api/v1/register/persons" | Create an HttpClient object. Set the url to "/api/v1/register/persons" e.g. RequestBuilder.setUri(httpUrl); |
"method": "POST" | Set this POST operaton to the HttpClient object e.g. RequestBuilder.create(methodName).setUri(httpUrl); |
"request": { ... } | Parse the request payload and set to HttpEntity. e.g. HttpEntity httpEntity = EntityBuilder.create().setContentType(APPLICATION_JSON).setText(reqBody).build(); |
| None. Nothing to do. | Parse the response to Java object or JSON String |
"verify": {JSON as-it-is} | Compare the actual response against expected field by field. - Use multiple assertThat(...). - Traverse through the response Object field by field - Or use JSON Path to extract value |
| Display all the mismatches and fail the test(time saver) | Stop at first mismatch and fail the test(unwanted delay in getting feedback) |
| Straight forward and easy | Step chaining is not straight forward |
Drawing a Simile
To draw a simile, we can pay attention to how docker-compose works. In docker-compose we tell the Docker-Compose framework(in a YAML file) to spin up certain things at certain ports etc, and then, things are done for us by the framework.
That's declarative way of doing things
e.g. of a compose YAML file
---
version: '2'
services:
zookeeper:
image: confluentinc/cp-zookeeper:5.0.1
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
kafka:
image: confluentinc/cp-kafka:5.0.1
depends_on:
- zookeeper
ports:
- 9092:9092
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181How neat and compact is that? Just think of it, for instance, if we had to write code/shell-scripts for the same repetitive tasks, how much hassle we would have gone through!
Example of a Zerocode YAML Test Scenario(more>>) (opens in a new tab) is below.
---
scenarioName: As simple GET request response
steps:
- name: "find_match"
url: "/api/v1/search/persons"
method: "GET"
request:
queryParams:
lang: "Amazing"
city: "Lon"
verify:
status: 200
body:
exactMatches: true
name: "Mr Bean"Testing Without Writing Code.
e.g.
That's the declarative way of validating an API what we discussed earlier
Test Case Fields
- Http(REST API and SOAP)
- Kafka (Produce, Consume RAW vs JSON)
- Java Function call e.g. DB SQL Executror
Http(REST API and SOAP)
SCENARIO
Scenario means a Test-Scenario or an User-Journey or a Use-Case during test automation. It is represented in the following way.
"scenarioName": "Free text - Validate a POST and GET method for a customer"LOOP
loop means the same Test-Scenario to be executed a number of times.
e.g.
"scenarioName": "Free text - A scenario"
"loop": 3IGNORESTEPFAILURES
When this DSL flag is set to true, the framework will go ahead and execute the subsequent steps in the scenario file.
"ignoreStepFailures": trueThis is an optional flag and you can skip this or set to false to retain the default behavior.
URL
REST endpoint or a SOAP end-point or a Kafka topic.
"url": "/api/v1/register/persons",Or you can mention the FQDN with http or https with port
"url": "https://apphost.gov.uk/api/v1/register/persons",See ahead examples on how you can point to a Kafka topic using this url field.
METHOD
REST end-point or SOAP end-point
All Http methods such as POST, PUT, GET, PATCH, DELETE etc
"method": "POST",Or when we need to call a Java function
"method": "executeSql",Or
when we need to validate Kafka events
"operation": "produce",
or
"operation": "consume",Note- method and operation are identical and can be used interchangeably. Preferably method is used for http calls and operation is used for Kafka calls.
(See Kafka DSLs below)
RETRY
Retry comes handy when the actual response doesn't match the expected values in certain use-cases.
"retry": {
"max": 5,
"delay": 2000
},The above settings will retry maximum of 5 times with 2sec delay between the retries.
If one fo the retries goes success(meaning if the actual response matches the expected response), then the framework will stop retrying further and come out of that step marking the it as PASSED.
CUSTOMLOG
Custom Log This is an optional field which can be used when user want custom log for particular step.
"customLog": "custom message"REQUEST
For REST end-point or SOAP end-point, request details with Headers and Body payload
"request": {
"body": {
"id": 1000,
"name": "Titan"
}
},Or when we need to call a Java function with a SQL query as method parameter
"request": "select id, name from customers"QUERYPARAMS
This DSL field can be used for sending query params to the HTTP endpoints.
"queryParams":{
"param1": "value1",
"param2": "value2"
}which is equivalent to ?param1=value1¶m2=value2
HEADERS
Request with headers and body payload,
"request": {
"headers": {
"X-GOVT-TOKEN": "90945"
},
"body": {
"id": 1000,
"name": "Titan"
}
},VERIFYMODE
verifyModeis STRICT or LENIENT
{
"verifyMode": "STRICT",
"verify":{
...
}
}When we specify STRICT mode, then the actual payload has to exactly match the expected payload.
LENIENT is the default mode even if we do not mention it.
VERIFY
Verifications and Assertions are used for the similar purpose where,
verifyis mostly used forverificationof an implementation against a Specassertionsis mostly used forvalidationan implementation
For REST services, we need to put the expected response with response Status, Headers and Body payload.
Only status validation
"verify": {
"status": 200
}or
"verify": {
"status": 200
}Or status and payload id assertions
Only status assertion
"verify": {
"status": 200,
"body": {
"id" : 583231
}
}Or partial or full payload assertions
"verify": {
"status": 200,
"body": {
"login" : "octocat",
"id" : 583231,
"type" : "User"
}
}Or with response headers details
"verify": {
"status": 200,
"headers":{
"Server":"sit2.hsbc.co.uk",
"X-HSBC-BANK":"$NOT.NULL" //<--- "$NOT.NULL" if value is undeterministic
},
"body": {
"login" : "octocat",
"id" : 583231,
"type" : "User"
}
}STATUS
For REST services or SOAP, we need to put the expected response with response Status, Headers and Body payload.
Only status assertion
"verify": {
"status": 200
}BODY
This is the payload, if present in the request section, then passed to the REST endpoint.
This is the payload, if present in the response section, then treated as expected response from the REST endpoint.
"verify": {
"status": 200
"body": {
"login" : "octocat",
"id" : 583231,
"type" : "User"
}
}Kafka
TOPIC
We mention the Kafka topic name
"url": "kafka-topic:heathrow-inbound",OPERATION
We need to mention produce or consume from/to a Kafka topic
"operation": "produce",or
"operation": "consume",REQUEST
We need to Produce or Consume to/from a Kafka topic,
- a
RAWrecord
"request": {
"records": [
{
"key": "key-101",
"value": "Hello Kafka"
}
]
},- a JSON record
"request": {
"recordType" : "JSON",
"records": [
{
"key": "key-101",
"value": {
"name" : "Jey"
}
}
]
},Or while Consuming we can specify whether to commitSync after consuming, recordType as RAW or JSON etc.
"request": {
"consumerLocalConfigs": {
"recordType" : "JSON",
"commitSync": true,
"maxNoOfRetryPollsOrTimeouts": 3
}
},ASSERTIONS
For Kafka services, we can put the expected response with response Status, RecordMetadata.
Only status assertion
"verify": {
"status": "Ok"
}Or status with recordMetadata assertion while Producing
"verify": {
"status": "Ok",
"recordMetadata": "$NOT.NULL"
}Or size with records assertion while Consuming
"verify": {
"size": 1,
"records": [
{
"key": 101,
"value": {
"name" : "Jey"
}
}
]
}HelloWorld Examples (Try at home)
- Http examples are here in GitHub-Http (opens in a new tab)
- Kafka examples are here in GitHub-Kafka (opens in a new tab)
- Java Function Call examples are here in GitHub-Java (opens in a new tab)
More About Kafka, DB, OAuth2, Http etc (Click to expand)
-
Many more HelloWorld examples (opens in a new tab), such as Spring boot app testing, Performance testing, Kotlin app testing etc.
The purpose of Zerocode lib is to make your API tests easy to write, easy to change, easy to share.
See the Table Of Contents (opens in a new tab) for usages and examples.
For Kafka testing approach, visit this page Kafka-Testing Quick Start (opens in a new tab).
Running the Tests using JUnit
- All examples above run via Junit
@Testannotation like below.
@TargetEnv("github_host.properties")
@RunWith(ZeroCodeUnitRunner.class)
public class JustHelloWorldTest {
@Test
@Scenario("helloworld/hello_world_status_ok_assertions.json")
public void testGet() throws Exception {
}
}You point to any JSON file and run. Hosts details are in the
.propertiesfile by@TargetEnv
- Also you can run as a
Suitepointing to the root of apackage.
Both Declarative and Extensible
While Zerocode framework is light-weight and simple to write test intentions in JSON/YAML format, at the same time we can customize/extend it to add our own flavours.
For instance, we can add custom Http Headers to the entire test-suite or an individual test-case, automate OAuth2 secured APIs, or use our own flavour of Apache Kafka Client to deal with Kafka Brokers and much more stuff.
...making all these things super easy and straight forward.