Before exploring what Karate has to offer, we'll need to add a few dependencies to our project (assuming you already have your Cucumber dependencies):
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-apache</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-junit4</artifactId>
<version>0.6.0</version>
</dependency>
pom.xml
We'll also integrate Karate with JUnit using @CucumberOptions and a simple java class in our test directory:
@RunWith(Karate.class)
@CucumberOptions(features = "classpath:karateTestFiles/myTests")
public class KarateTest {}
KarateTest.java
Now let's say our project is given the following application.yaml file:
spring:
myDatabase:
url: jdbc:postgresql://myDatabaseUrl:5432/myDatabase
driver: org.postgresql.Driver
username: test
password: 123
server:
port: 8080
application.yaml
Using Karate, we can access elements of the above YAML within our Cucumber feature files using the read() function:
Feature: My Application Acceptance Tests
Background:
* def config = read('application.yaml')
* def dbUrl = config.spring.myDatabase.url
* def dbDriver = config.spring.myDatabase.driver
* def dbUsername = config.spring.myDatabase.username
* def dbPassword = config.spring.myDatabase.password
Scenario: My First Scenario....
Given ......
myAwesomeFeature.feature
Now let's say we have the following JSON file in our test resources that we would like to re-use in multiple tests:
{
"id": 123,
"name": "John Doe"
}
mySample.json
We can in turn access this JSON using the same read() function:
Feature: My Application Acceptance Tests
Background:
* def json = read('classpath:json/mySample.json')
Scenario: My First Scenario....
Given ......
myAwesomeFeature.feature
We can even change elements of this JSON at will:
Feature: My Application Acceptance Tests
Background:
* def json = read('classpath:json/mySample.json')
* json.id = 456
* json.name = "Billy Wilder"
myAwesomeFeature.feature
Pretty cool! Now, what if we wanted to create a database connection in the Cucumber file? First, we'll need to add some more dependencies to our project. In our case, we'll be needing a PostgreSQL connection:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>42.2.5</version>
</dependency>
pom.xml
Now we'll need some kind of java utilities class to handle the database connection work:
public class DbUtils {
private static final Logger logger = LoggerFactory.getLogger(DbUtils.class);
private JdbcTemplate jdbc;
public DbUtils(String url, String driver, String username, String password) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
jdbc = new JdbcTemplate(dataSource);
logger.info("initialize jdbc template:{}", url);
}
public Map<String, Object> readRow(String query) {
return jdbc.queryForMap(query);
}
public List<Map<String, Object>> readRows(String query) {
return jdbc.queryForList(query);
}
}
DbUtils.java
This should be enough to read from the database by instantiating an instance of DbUtils, using Java.type() as a means of telling Karate that we'd like to use this class:
Feature: My Application Acceptance Tests
Background:
* def config = read('application.yaml')
* def dbUrl = config.spring.myDatabase.url
* def dbDriver = config.spring.myDatabase.driver
* def dbUsername = config.spring.myDatabase.username
* def dbPassword = config.spring.myDatabase.password
* def DbUtils = Java.type('com.myApplication.DbUtils')
* def myDatabase = new DbUtils(dbUrl, dbDriver, dbUsername, dbPassword)
myAwesomeFeature.feature
Now, what if our feature file was for whatever reason dependent upon another feature file to run prior to executing our upcoming tests? Perhaps another set of tests that do some database insertion or authorization setup. We can use the call() function to execute an entirely separate feature file:
Feature: My Application Acceptance Tests
Background:
* def someOtherFeatureFile = call read('classpath:features/myOtherTests.feature')
myAwesomeFeature.feature
Neat! Let's take a look at our Cucumber file so far:
Feature: My Application Acceptance Tests
Background:
* def config = read('application.yaml')
* def dbUrl = config.spring.myDatabase.url
* def dbDriver = config.spring.myDatabase.driver
* def dbUsername = config.spring.myDatabase.username
* def dbPassword = config.spring.myDatabase.password
* def DbUtils = Java.type('com.myApplication.DbUtils')
* def myDatabase = new DbUtils(dbUrl, dbDriver, dbUsername, dbPassword)
* def someOtherFeatureFile = call read('classpath:features/myOtherTests.feature')
myAwesomeFeature.feature
We've got full access to our application.yaml, a connection to our local database, and assurance that myOtherTests.feature will be executed prior to running any of our upcoming acceptance tests. Speaking of which, let's write one of those now:
Feature: My Application Acceptance Tests
Background:
.......
Scenario: Testing GET endpoint
* configure headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }
Given url 'http://localhost:8080/myApplication/accounts'
And header Authorization-Header = config.authorizationToken
When method GET
Then status 200
myAwesomeFeature.feature
Thanks to Karate's simplicity, much of the above is probably pretty self-explanatory, but we'll go through this line by line just in case. First, notice the "configure headers" line. This tells Karate that we're about to test one of our endpoints, and that test is going to require some headers. We're able to define those headers in pure JSON form. Then, Karate recognizes "url" as a keyword within our Given statement, and understands that the following string is the endpoint we'll want to be calling in our test. We can also explicitly define headers one by one, which is what the "And header Authorization-Header..." line is doing. Now, when we reach "When method GET", Karate makes the GET call to the given url, with our defined headers, all without any additional java test classes or dependencies. Pretty neat!
Lastly, you'll notice the "Then status 200" line. This is to assert that the response to our GET call was indeed a 200. Let's add some more assertions to make sure our endpoint is working exactly as expected:
Feature: My Application Acceptance Tests
Background:
.......
Scenario: Testing GET endpoint
* configure headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }
Given url 'http://localhost:8080/myApplication/accounts/123'
And header Authorization-Header = config.authorizationToken
When method GET
Then status 200
#assert entire json response
And match $ == #object
And match $ == { id: "123", name: "Jane Doe" }
#or, assert specific json properties
And match $.id != null
And match $.id == "123"
And match $.name == #string
And match $.name == "Jane Doe"
myAwesomeFeature.feature
As you can see, not only can we assert the status code of the response, but we can assert types (check out Karate's documentation for other type assertions), the JSON response itself, and any elements within it that we choose.
What if we wanted to also send a request body along with our endpoint call, say, for a POST?
Feature: My Application Acceptance Tests
Background:
.......
Scenario: Testing POST endpoint
Given url 'http://localhost:8080/myApplication/accounts'
And request { id: "123", name: "Jane Doe" }
When method POST
Then status 200
And match $ == #object
And match $ == { id: "123", name: "Jane Doe" }
myAwesomeFeature.feature
All we have to do is change our GET to a POST, then add the content of our request body to our Given. That's it! Now we'll try to add an assertion to double-check that our POST properly inserted data into our database:
Feature: My Application Acceptance Tests
Background:
.......
* def DbUtils = Java.type('com.myApplication.DbUtils')
* def myDatabase = new DbUtils(dbUrl, dbDriver, dbUsername, dbPassword)
.......
Scenario: Testing POST endpoint
Given url 'http://localhost:8080/myApplication/accounts'
And request { id: "123", name: "Jane Doe" }
When method POST
#query database to ensure account properly inserted
* def accountsTableRow = myDatabase.readRow("Select * from accounts where id = " + $.id)
And match accountsTableRow != null
And match accountsTableRow.name == $.name
myAwesomeFeature.feature
Because we have our myDatabase object thanks to DbUtils, we're able to simply pass SQL statements straight into our DbUtils methods, returned in JSON form (Note: because our DbUtils readRow() method returns a Map<String, Object>, Karate converts this into a JSON for easy access). No Spring bean @Autowiring or additional Java test classes needed!
You probably noticed that Karate ends up making your feature file a bit less readable to the average business-level non-developer. That is something you'll want to consider when determining whether it's the right tool for your project. But for developers, its magic has the potential to speed up the creation of your acceptance-tests. Karate is also open source, and we encourage you to check out its GitHub (linked below) and documentation to see all it has to offer. Thanks for reading!