DEV Community

Trey Pero
Trey Pero

Posted on

Level up your Karate Testing with Spring Boot DI

For a few years I've used Cucumber for higher level testing, and only more recently started using Karate. While Cucumber is a great tool, I think Karate really shines in reducing the boilerplate that comes along with step definitions and making it easy to write meaningful tests quickly, especially when it comes to API testing.

For simple applications, writing your feature files in plain JavaScript will suffice. As your application and tests grow, reusing some Java code can become valuable though. Spring Boot APIs can benefit a lot from Karate testing, but what about leveraging the power of Spring Boot directly in your Karate tests?

Some example use cases

  • While Karate supports configuration via karate-config.js files, some may like configuring via Spring YAML/properties instead. This can be helpful to reconfigure things outside of rebuilding code as well.
  • To sync certain Spring Boot configuration properties between application and tests.
  • To verify database state between API calls. JPA repository/entity beans could be used in the Karate tests.
  • Some Spring beans may simply be useful to utilize in tests.

How to integrate Spring into Karate

Full sample project: https://github.com/trey-pero/karate-spring

Karate can be executed via a simple JUnit test. To begin wiring Spring in, setup the JUnit test as a @SpringBootTest.

@RequiredArgsConstructor
@SpringBootTest(classes = Main.class)
public class KarateTest {
    private final ApplicationContext applicationContext;

    @Test
    void test() {
        ApplicationContextHolder.setApplicationContext(this.applicationContext);

        // Since this one JUnit test runs all Karate tests,
        // fail the test if any underlying Karate tests fail
        assertEquals(0, Runner.path("classpath:org/tpero")
                .parallel(Optional.ofNullable(System.getProperty("karate.threads"))
                        .map(Integer::parseInt)
                        .orElse(5)
                ).getFailCount());
    }
}
Enter fullscreen mode Exit fullscreen mode

In order to access the Spring context (which provides access to all beans and configuration), it needs to be stored somewhere that Karate can access it statically.

/**
 * Provides Karate static access to the Spring application context.
 */
@UtilityClass
public class ApplicationContextHolder {
    @Setter
    @Getter
    private ApplicationContext applicationContext;
}
Enter fullscreen mode Exit fullscreen mode

From Karate config, the static holder can be accessed to wire the application context into Karate's global configuration map using the following sample:

/**
 * Define common feature file configuration here.
 * @returns Common configuration as a JSON object.
 */
function getConfig() {
    // Global values
    const appContext = Java.type("org.tpero.ApplicationContextHolder")
        .getApplicationContext()
    const environment = appContext.getEnvironment()

    return {
        appContext: appContext,
        environment: environment,

        baseUrl: `http://localhost:${environment.getProperty('app.server.port', '8080')}`
    }
}
Enter fullscreen mode Exit fullscreen mode

With the above setup code, beans and configuration can be accessed from Karate feature files, shown by this sample which tests a simple login API that returns a JWT token.

Feature: Login
  Background:
    * url baseUrl
    * path '/login'
    # Load the JWT service bean from Spring DI
    * def jwtService = appContext.getBean('jwtService')

  Scenario: Login with valid credentials
    Given request { username: 'user', password: 'password' }
    When method post
    Then status 200
    * print response

    # Use the JWT service bean to decode the JWT from the response
    * def decodedJwt = jwtService.decode(response)
    * print decodedJwt
    * def decodedBody = decodedJwt.getBody()
    * print decodedBody

    And match decodedBody['sub'] == 'user'
    * def issuedAt = Number(decodedBody['iat'])
    # Ensure the issuedAt is in the past
    And assert issuedAt < Java.type('java.lang.System').currentTimeMillis()
    * def configuredExpirationInMinutes = Number(environment.getProperty('jwt.expiration.ms')) / 1000
    # Ensure the expiration is the configurable amount of minutes beyond the issuedAt
    And match Number(decodedBody['exp']) == issuedAt + configuredExpirationInMinutes
Enter fullscreen mode Exit fullscreen mode

This sample demonstrates how easy it is to integrate the power of Spring Boot into Karate to build more capable test suites.

Top comments (0)