Gaining Confidence

Frederik Hahne

Qualitätssicherung & Testen OWL/Paderborn

April 4th, 2019

About Me

Frederik Hahne

@atomfrede

Software Developer @wescalehq

@java_hipster board member

@jugpaderborn organizer

@devoxx4kids mentor

Disclaimer

This talk was created for JUG talk and with coding demos and takes usually 1.5 hours

But not today

Gaining Confidence

Context

  • a lot of (micro)services
  • api first
  • diverse stack
https://www.martinfowler.com/articles/microservice-testing/#conclusion-test-pyramid
https://labs.spotify.com/2018/01/11/testing-of-microservices/

Integration Testing

  • Do not rely on the correctness of other services
  • Database (Mocks)
  • Service Mocks
  • API/Contracts

But still

Give us confidence that the code does what it should.

Provide feedback that is fast, accurate, reliable and predictable.

https://labs.spotify.com/2018/01/11/testing-of-microservices/

Databases
Services
APIs

Databases

  • Mock/Stub Database
  • in memory database
  • mongo
  • solr, elastic
  • jsonb in psql

Testcontainers

  • Database Containers
  • Selenium Containers
  • Kafka, Elastic, ...
  • Toxi Proxy
  • Generic Containers
  • Docker Compose
  • Dockerfiles
@Autowired
public JooqProductRepository(DSLContext jooq) {
  this.jooq = jooq;
}

@Override
public Optional<Product> findOneById(Long id) {
       
  return jooq.selectFrom(PRODUCT_TABLE)
         .where(PRODUCT_TABLE.ID.eq(id))
         .fetchOptional()
         .map(record -> recordMapper.toProduct(record));
}
@JooqTest
class JooqProductRepositoryIT extends Specification {

  @Autowired
  DSLContext jooq

  def 'fetch non existing product by id'() {

    given:
    def subject = new JooqProductRepository(jooq)

    when:
    def result = subject.findOneById(4711)

    then:
    result != null
    result.isEmpty()
  }
}

Demo


spring.datasource.url=jdbc:tc:postgresql:11.0://localhost:5432/confidence
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver

@Testcontainers
class JooqProductRepositoryIT extends Specification {

  @Shared
  PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .withDatabaseName("confidence")
    .withUsername("confidence")
    .withPassword("confidence")

  @Shared
  DataSource datasource

  def setupSpec() {

    HikariConfig config = new HikariConfig()
    config.setUsername(postgreSQLContainer.username)
    config.setJdbcUrl(postgreSQLContainer.jdbcUrl)
    config.setPassword(postgreSQLContainer.password)
    datasource = new HikariDataSource(config)

    Flyway flyway = new Flyway().configure()
      .dataSource(datasource)
      .load()
    flyway.migrate()
  }

 def '...'() {
    
   given:
   def subject = new JooqProductRepository(DSL.using(datasource, SQLDialect.POSTGRES))
 }

Title Text

There is more...

  • Testing interaction with other services
  • Wrap call into an adapter
  • Mock the adapter
  • Returns POJOs
  • Test your logic

Not Good Enough

  • De/Serialization
  • Handling of Web Exceptions
  • Caches, Retries, ...
  @Rule
  WireMockRule wireMockRule = new WireMockRule(options().dynamicPort())

  def "should load ratings"() {

    given:
    def subject = new ProductRatingConnector(new OkHttpClientFactory(), 
                      "http://localhost:${wireMockRule.port()}")
    and:
    //language=json
    def successBody = '''[
  {
    "id": 1,
    "title": "Nice Product",
    "rating": 5,
    "description": "Lorem Ipsum",
    "productId": 1
  }
]
'''
    and:
    stubFor(get(urlPathEqualTo("/reviews"))
      .withQueryParam("productId", equalTo("1"))
      .willReturn(aResponse()
      .withStatus(200)
      .withBody(successBody)))

    when:
    def result = subject.fetchProductRatings(1)

    then:
    result != null
    result.size() == 1

  }

APIs

  • OpenAPI
  • Works as expected
  • Breaking Changes
  • Implementation adhere to spec

How?

  • Spring Boot Test
  • REST Assured
  • Swagger Request Validator

A Java library for validating HTTP request/responses against an OpenAPI / Swagger specification.

https://bitbucket.org/atlassian/swagger-request-validator
  TestFilters(String swaggerFileLocation) {

    def file = new File(swaggerFileLocation)

    def map = file.withReader {
      Yaml yaml = new Yaml()
      return yaml.load(it)
    }

    def whitelist = ValidationErrorsWhitelist.create()
      .withRule("Ignore 'application/problem+json' does not match any allowed types.",
      allOf(
  messageContains("Response Content-Type header 'application/problem\\+json' does not match any allowed types.*")))

    def interactionValidator = OpenApiInteractionValidator
      .createFor(new JSONObject(map).toString())
      .withWhitelist(whitelist)
      .build()

    swaggerValidationFilter = new OpenApiValidationFilter(interactionValidator)
  }
  @Test
  void create_product() {

    //language=json
    def productCreate = '''
{
  "name": "Thinkpad T480s",
  "description": "A thinkpad, as simple as that"
}
'''

    given()
    .contentType(ContentType.JSON)
    .body(productCreate)
    .when()
    .post("/products")
    .then().statusCode(201)

  }
@ClassRule
static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.0")
    .withDatabaseName("confidence")
    .withUsername("confidence")
    .withPassword("confidence")
    .withNetwork(Network.SHARED)
    .withNetworkAliases("postgres")

static GenericContainer bootJar

@BeforeClass
static void setup() {
    bootJar = new GenericContainer(new ImageFromDockerfile().withDockerfileFromBuilder(
      { builder ->
        builder.from("openjdk:11-jdk-stretch")
          .volume("/tmp")
          .copy("confidence-0.0.1-SNAPSHOT.jar", "app.jar")
          .entryPoint("java", "-Dspring.datasource.password=confidence",
          "-Dspring.datasource.url=jdbc:postgresql:postgres:5432/confidence",
          "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar")
          .build()
      })
      .withFileFromPath("confidence-0.0.1-SNAPSHOT.jar", 
            TestPaths.resolve("../../../build/libs/confidence-0.0.1-SNAPSHOT.jar")))
      .withExposedPorts(8080)
      .withNetwork(postgreSQLContainer.getNetwork())
    bootJar.start()
}

@Before
void clear() {
    RestAssured.baseURI = "http://${bootJar.getContainerIpAddress()}:${bootJar.getMappedPort(8080)}/v1"
}

Request/Response Validation

  • Works your implementation as expected?
  • Does the request match the specification?
  • Does the response and codes match the specification?

Testcontainers helped us

  • to test legacy apps
  • to protect our apis against regressions
  • have fun with integration tests
  • extend our range of tests
  • but be careful no to forget unit tests and testable design!

Keep your tests

One More Thing...

def "failed login is visualized"() {

    given:
    to GebLoginPage

    when: "user logs in with incorrect password"
    loginPage.login("user", "wrongpassword")

    then: "the user stays on the login page"
    at GebLoginPage

    and: "a generic error message is shown"
    find { $(".e2e__login-failed-message") }

}

def "successful login forwards to index page"() {

    given:
    to GebLoginPage

    when: "user logs in"
    loginPage.login("user", "user")

    then: "the user is forwarded to index page"
    at GebIndexPage

}

Questions?

Resources

  • https://slides.com/kiview/testcontainers-whs
  • https://github.com/testcontainers/testcontainers-java
  • https://gitlab.com/atomfrede/confidence
  • https://slides.com/kiview/testcontainers-intro

Living Documentation

Mittwoch, 09.05.2019

18.00 Uhr

Eclipse MicroProfile

Mittwoch, 25.06.2019

18.30 Uhr

Integration Testing Done Right

Donnerstag, 10.10.2019

18.00 Uhr

Made with Slides.com