Introduction to Karate

Have you ever wanted to use JavaScript or execute code from within your Cucumber feature files? Or wished for a simpler way to test REST endpoints without an additional Java test class for handling these calls? Enter Karate, a test-automation framework built on top of Cucumber that gives our feature files a bit more coding freedom during acceptance testing, as opposed to the usual basic Given/When/Then formula.

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!

GitHub - intuit/karate: Test Automation Made Simple
Test Automation Made Simple. Contribute to intuit/karate development by creating an account on GitHub.
Author image
Mike Mitchell is a Backend Software Engineer for Ippon Technologies primarily focused in Java and the Spring framework.
Richmond, Va LinkedIn
OUR COMPANY
Ippon Technologies is an international consulting firm that specializes in Agile Development, Big Data and DevOps / Cloud. Our 400+ highly skilled consultants are located in the US, France, Australia and Russia. Ippon technologies has a $42 million revenue.