Karate is an automation framework designed to make automation testing easy, super readable and more reliable than other offerings in the open source space - don’t even ask me how many times I’ve been bitten by Selenium’s reluctance to play nice with UI elements or been snowed under trying to get my head around a complex multi-util, multi-file test scenario.
Karate is essentially a Gherkin-like programming language, with the ability to use Java and JavaScript almost seamlessly in order to handle more complex and unique functionality should you require it. It’s primarily an API Automation tool, but it handily also provides very good UI Automation capabilities, Load Testing, Kafka Testing and Image Comparison Testing, amongst other features.
There’s a whole host of interesting and handy features of the framework, but I’ll focus on a few that make API testing easy, efficient and clear.
Test Readability
I’ll preface this with the fact Cucumber is more readable - but that requires a huge amount of effort in implementing the step-definitions correctly, which means higher time investment and ultimately less readable code in the background.
Karate lets us write our test scenarios straight out the box - there’s no need to write Java glue code or step-definitions like Cucumber. The example below showcases this perfectly - there’s no additional setup needed or extra digging into the technical, behind-the-scenes implementation in order to get testing our services.
Let’s take a look - say we want to send a request to an endpoint to list users (we’ll use reqres for the examples in this post):
Feature: 'users' endpoint Scenarios
Background:
* baseUrl 'https://reqres.in/api'
Scenario: Given we send a request to the 'users' endpoint, a 200 response is returned
Given path 'users'
When method GET
Then status 200
And match response.data[*].first_name contains "Tobias"
The above example kicks things off with our Feature File format:
- Feature - the piece of functionality or “feature” we’ll be testing
- Background - this is code that is run before every Scenario, save for special cases where
call once
orcallSingle
is used (more on that later). - Scenario - our sequence of steps in order to test functionality
Firstly, we set our baseUrl
in the Background
section so that our HTTP requests know where to send the requests to.
We can easily override this in any given Scenario if we so wished to change our target API.
Typically, within our Scenario’s, we’ll follow a Given, When, And, Then
syntax - which is 100% interchangeable. These
keywords don’t have any underlying functionality other than to tell Karate we’re at the start of a new instruction. The
Karate way here is to use these Gherkin style keywords in the most readable way that makes sense to you and your test
flows. Here’s a breakdown of the request:
Given path 'users'
means we’ll be targetinghttps://reqres.in/api/users
in our request -path
is basically a concatenation ofbaseUrl
and whatever followsWhen method GET
means we’ll be sending a GET request - as soon as we specify themethod
, Karate will send the requestThen status 200
is an assertion on the HTTP response codeAnd match response.data[*].first_name contains "Tobias"
is another assertion, this time on the array of JSON objects from the response to contain an occurrence of{ "first_name": "Tobias" }
- if our test was deterministic in the array index of the object that contained “Tobias”, then we could specify that instead of[*]
That’s all we need - this shows just how easy it is to write our test scenarios and how readable they can be, meaning less time is spent understanding the scenarios and more time finding bugs!
Feature File Re-use, Call Single and Caching
This next feature is both a nice saver in terms of execution cost/time but also in terms of code re-usability. It’s true
that this is not necessarily a great pattern in terms of test readability, but there certainly still is a time and place
for it.
Say we want to add some authentication into the mix - we would request a token, and then add that as a header in all subsequent requests. How we approach that in Karate looks like this:
Feature: 'users' endpoint Scenarios
Background:
* baseUrl 'https://reqres.in/api'
Scenario: Grab a valid authentication token and send a request to the 'users' endpoint
Given path 'login'
And request { username: 'eve.holt@reqres.in', password: 'cityslicka' }
When method POST
Then status 200
* def token = response.token
Given path 'users'
And header Authorization = `Bearer ${token}`
When method GET
Then status 200
This Scenario contains two API calls - the first of which is a POST request to the login
path with some valid known
credentials. We also assert that this request returns a HTTP 200 response code.
We then have an example of how we can define variables within Karate - * def token = response.token
. *
is also
interchangeable with our Gherkin syntax. Here, we are simply saving the token from the response so we can use it later.
response
is always a reference to the most recent response body.
The second API call is then the same as the first example we went through, but with the addition of an Authorization header.
Obviously once our test suite grows arms and legs, we don’t want to authenticate for every single request, especially
for large complex test flows that are happy with the same authorization token - enter callSingle
.
Karate allows us to move any piece of commonly used functionality out into a Feature File that can be called (via
call
, call once
or callSingle
). Here, we can move the authentication request into a callSingle
call and put it
into the Background
. Usually, all code within the Background
is executed for every Scenario in that Feature File,
but in the case of callSingle
, we only run it once across all Features that make the same call.
For example, we have created AuthenticateAs.feature
which contains our request to login and store the token into a
token
variable.
# AuthenticateAs.feature
Feature: Util for authenticating as a user and providing an auth token
Background:
* baseUrl 'https://reqres.in/api'
Scenario: Send a request to login and save the token
Given path 'login'
And request { username: '#(username)', password: '#(password)' }
When method POST
Then status 200
* def token = response.token
'#(username)'
is how we tell Karate to use any JSON values that were passed in whilst calling the file.
Now in the example below, we can call to the AuthenticateAs.feature
file by using karate.callSingle()
. We also pass
it a JSON object containing some parameters that we want to use for our authentication. Lastly, we assign all of this to
a local variable called auth
- meaning that we can access any variables defined in AuthenticateAs.feature
via
auth.variable
.
Feature: 'users' endpoint Scenarios
Background:
* baseUrl 'https://reqres.in/api'
* karate.configure('callSingleCache', { minutes: 4 })
* def auth = karate.callSingle('AutenticateAs.feature', { username: 'eve.holt@reqres.in', password: 'cityslicka' })
Scenario: Given we send a request to the 'users' endpoint, a 200 response is returned
Given path 'users'
And header Authorization = `Bearer ${auth.token}`
When method GET
Then status 200
Now we can have a series of requests and scenarios that only authenticate once, since we’ve used karate.callSingle()
-
we’ve also configured our callSingleCache
to refresh every 4 minutes to avoid tokens reaching their expiry. This will
not only speed up execution but also improve test readability since our Scenarios are even more straight to the point on
what they are testing.
I previously mentioned we also have call
and callOnce
, with their main purpose to facilitate code re-use. These can
be used similarly to callSingle
, with call
happening for every Scenario inside your Feature and callOnce
only once
inside the Feature file. call
is especially handy when you have a complex flow of requests and test steps that “muddy”
the waters of a Scenario that you can just move out into another file and replace with a one-liner.
Within the world of development, we try to re-use code as much as possible - but within test automation this can seriously hamper test readability and time spent understanding, debugging and fixing your tests. Tests should be clear on what they are testing and if that means we have to re-use code then I think that’s absolutely fine. In my experience of using Karate, I feel that it provides a really nice middle-ground that empowers the tester to make the decision on how readable their test scenarios are.
Hybrid Scenarios (plus a sneak peek into Karate UI Automation)
Let’s now touch on Hybrid Scenarios briefly. So far everything has been API focussed, but I want to show how we can easily incorporate a simple UI test alongside API calls.
Say we want to test logging into our application via the UI - but first we need to create our user - what would this look like in terms of an actual Scenario?
Feature: Check that we can Login using the UI
Background:
* baseUrl 'https://reqres.in/api'
Scenario: Given we create a user, we can successfully login via the UI using the same user
Given path 'register'
And request { email: 'eve.holt@reqres.in', password: 'pistol' }
When method POST
Then status 200
* baseUrl 'https://reqres.in/ui'
# "https://reqres.in/ui/login" doesn't actually exist, but used just as an example
Given driver `${baseUrl}/login`
And waitFor(usernameLocator).input('eve.holt@reqres.in')
And waitFor(passwordLocator).input('pistol')
When waitFor(signInButtonLocator).click()
Then waitForUrl(`${baseUrl}/home`)
Firstly, we have our POST request to create the new user via the register
endpoint. We then need to tell Karate that
our baseUrl
has now changed to the UI because when we instantiate a
driver instance, it’ll automatically navigate to the
currently configured baseUrl
.
Karate’s UI test features are really robust in the sense of avoiding flakey tests and waiting on elements appearing on
screen. One example is waitFor(locator)
- which will wait until a given locator is present on screen. We can also
chain commands on the back of the locator being found as opposed to waiting for the locator, storing it, then performing
an action.
This means we have a tidy set of steps that will spin up our driver, wait for elements to appear, perform actions accordingly and then wait for the URL to change to the home page to show we’ve logged in with our new user successfully.
Dynamic Scenario Outlines
Lastly, I want to talk about Dynamic Scenario Outlines.
If you’re used to Cucumber then you’ve probably got an understanding of Scenario Outlines - they let you run through the exact same test steps but with your variables and data driven directly from a table.
The example below is 2 different Scenarios, but for each iteration (or row) we substitute the values in angle-brackets with values from each column of the table. This is really powerful when we have a lot of different tests that follow the same steps, but with different input data each time, meaning big savings on lines of code and improved test maintainability.
Feature: 'register' endpoint Scenarios
Background:
* baseUrl 'https://reqres.in/api'
Scenario Outline: Given we send a <Scenario> username and password combination, the 'register' endpoint returns a <Status>
Given path 'register'
And request { username: '< Username >', password: '< Password >' }
When method POST
Then status < Status >
And match response == < Response >
Examples:
| Scenario | Username | Password | Status | Response |
| valid | eve.holt@reqres.in | pistol | 200 | { "id": 4, "token": "QpwL5tke4Pnpja7X4" } |
| invalid | eve.holt@reqres.in | | 400 | { "error": "Missing password" } |
In this example, the first row means we’ll send a POST request containing a valid username and password to the
register
endpoint. We then assert on a valid 200
response from the endpoint and also do a response body assertion.
The second row will send a request without password information, which this time should result in a 400
response from
the endpoint and an error in the response body.
However, we can do something really cool with Dynamic Scenario Outlines. We can essentially generate our Examples:
table at run-time.
For some initial context, here’s a snippet of the response from https://reqres.in/api/users
:
{
"page": 1,
"per_page": 6,
"total": 2,
"total_pages": 1,
"data": [
{
"id": 7,
"email": "michael.lawson@reqres.in",
"first_name": "Michael",
"last_name": "Lawson",
"avatar": "https://reqres.in/img/faces/7-image.jpg"
},
{
"id": 8,
"email": "lindsay.ferguson@reqres.in",
"first_name": "Lindsay",
"last_name": "Ferguson",
"avatar": "https://reqres.in/img/faces/8-image.jpg"
}
]
}
Let’s say we wanted to clean up our test environment of users, but in order to do that we need to know all user
ID’s and then send a DELETE request to https://reqres.in/api/users/${id}
for each one.
In the below example, we have a @setup
Scenario which is our data setup for our Scenario Outline.
Up until now we are used to the Background
section running before every Scenario, but anything tagged with @setup
will actually run before the Background
. This means we need to set our baseUrl
within the @setup
scenario. We’ll
still want to keep our baseUrl
defined within the Background
since any other scenarios still need to know the target
URL.
The @setup
section then sends a GET request to the
https://reqres.in/api/users
endpoint and then we save the data
array from the response into a userData
variable.
Now, within our Examples table, we can give our Scenario Outline access to the userData
array from the @setup
Scenario by using karate.setup().userData
. This means that our Scenario Outline is aware of each JSON object within
the array, and it automatically has access to all values from each object and hence the id
key-value pair.
The steps within the Scenario Outline then make a DELETE request to the users/${id}
path, and we assert on a 204
response meaning we successfully initiated a DELETE on that specific user ID.
Feature: Delete Users using Dynamic Scenario Outline
Background:
* baseUrl 'https://reqres.in/api'
@setup
Scenario:
* baseUrl 'https://reqres.in/api'
Given path 'users'
When method GET
Then status 200
* def userData = response.data
Scenario Outline: Delete all users by fetching the ID's from the users endpoint
Given path `users/${id}`
When method DELETE
Then status 204
Examples:
| karate.setup().userData |
If our ID’s were predictable then we could have used something like we have below, but when they are generated at
run-time then we require a @setup
routine in order to fetch our non-deterministic data.
Examples:
| IDs |
| 7 |
| 8 |
This is great for running through a bulk set of tests where we don’t necessarily know some vital inputs before execution. There’s a few others ways to do something similar in Karate, but another benefit of Dynamic Scenario Outlines is that Karate will still respect any parallel execution configuration, meaning we can run this across multiple threads with no extra config outside of the parallel runner configuration.
Conclusion
I hope you’ve enjoyed some brief examples of a few features of the Karate Test Automation framework that make API (and UI!) testing really easy, readable and accessible. Testing complicated back-end services, which typically are not very human-friendly without an interactive UI, doesn’t need to be complicated - Karate gives us scope to translate this layer of testing into clean and concise test scenarios that hopefully improves the test experience for everyone.
We have only scratched the surface in terms of everything that the framework offers and in a subsequent blog we can take a deeper dive into some of the other key features such as exploring the comprehensive assertion features, image comparison, built in HTML reporting or data driven tests via tags.
The documentation for Karate is really solid, with lots of examples and good explanations of each bit of functionality. There’s also a very active community on Stack Overflow asking and answering questions - you’ll find the author of Karate answering a lot of queries. Lastly, I’ve provided a link to a blog from Automation Panda about Gherkin which provides a comprehensive overview of the language.