Skip to main content

Introduction to Karate

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.
Post by Mike Mitchell
September 1, 2021

Comments