Skip to content

HttpClient

    dependencies {
        testImplementation("com.trendyol:stove-testing-e2e-http:$version")
    }

Configure

After getting the library from the maven source, while configuring TestSystem you will have access to http

TestSystem()
  .with {
    http {
      HttpClientSystemOptions(
        baseUrl = "http://localhost:8080",
      )
    }
  }
  .run()

The other options that you can set are:

data class HttpClientSystemOptions(
  /**
   * Base URL of the HTTP client.
   */
  val baseUrl: String,

  /**
   * Content converter for the HTTP client. Default is JacksonConverter. You can use GsonConverter or any other converter.
   * If you want to use your own converter, you can implement ContentConverter interface.
   */
  val contentConverter: ContentConverter = JacksonConverter(StoveSerde.jackson.default),

  /**
   * Timeout for the HTTP client. Default is 30 seconds.
   */
  val timeout: Duration = 30.seconds,

  /**
   * Create client function for the HTTP client. Default is jsonHttpClient.
   */
  val createClient: () -> io.ktor.client.HttpClient = { jsonHttpClient(timeout, contentConverter) }
)

Usage

GET Requests

Making GET requests with various options:

TestSystem.validate {
  http {
    // Simple GET request with type-safe response
    get<UserResponse>("/users/123") { user ->
      user.id shouldBe 123
      user.name shouldBe "John Doe"
    }

    // GET with query parameters
    get<String>("/api/index", queryParams = mapOf("keyword" to "search-term")) { response ->
      response shouldContain "search-term"
    }

    // GET with headers
    get<UserProfile>("/profile", headers = mapOf("X-Custom-Header" to "value")) { profile ->
      profile.email shouldNotBe null
    }

    // GET with authentication token
    get<SecureData>("/secure-endpoint", token = "jwt-token".some()) { data ->
      data.isAuthorized shouldBe true
    }

    // GET multiple items (list response)
    getMany<ProductResponse>("/products", queryParams = mapOf("page" to "1", "size" to "10")) { products ->
      products.size shouldBe 10
      products.first().name shouldNotBe null
    }
  }
}

GET with Full Response Access

When you need access to status code and headers:

TestSystem.validate {
  http {
    getResponse<UserResponse>("/users/123") { response ->
      response.status shouldBe 200
      response.headers["Content-Type"] shouldContain "application/json"
      response.body().id shouldBe 123
    }

    // Bodiless response (only status and headers)
    getResponse("/health") { response ->
      response.status shouldBe 200
    }
  }
}

POST Requests

Various POST request patterns:

TestSystem.validate {
  http {
    // POST with request body and expect JSON response
    postAndExpectJson<UserResponse>("/users") {
      CreateUserRequest(name = "John", email = "john@example.com")
    } { user ->
      user.id shouldNotBe null
      user.name shouldBe "John"
    }

    // POST and expect bodiless response (only status)
    postAndExpectBodilessResponse(
      uri = "/products/activate",
      body = ActivateRequest(productId = 123).some()
    ) { response ->
      response.status shouldBe 200
    }

    // POST with full response access
    postAndExpectBody<ProductResponse>(
      uri = "/products",
      body = CreateProductRequest(name = "Laptop", price = 999.99).some()
    ) { response ->
      response.status shouldBe 201
      response.headers["Location"] shouldNotBe null
      response.body().id shouldNotBe null
    }

    // POST with headers and token
    postAndExpectJson<OrderResponse>(
      uri = "/orders",
      body = CreateOrderRequest(items = listOf("item1", "item2")).some(),
      headers = mapOf("X-Request-ID" to "12345"),
      token = "jwt-token".some()
    ) { order ->
      order.id shouldNotBe null
      order.status shouldBe "CREATED"
    }
  }
}

PUT Requests

Update operations with PUT:

TestSystem.validate {
  http {
    // PUT with response body
    putAndExpectJson<UserResponse>("/users/123") {
      UpdateUserRequest(name = "Jane Doe", email = "jane@example.com")
    } { user ->
      user.name shouldBe "Jane Doe"
      user.email shouldBe "jane@example.com"
    }

    // PUT without response body
    putAndExpectBodilessResponse(
      uri = "/products/123",
      body = UpdateProductRequest(name = "Updated Product").some()
    ) { response ->
      response.status shouldBe 200
    }

    // PUT with full response access
    putAndExpectBody<ProductResponse>(
      uri = "/products/456",
      body = UpdateProductRequest(price = 899.99).some()
    ) { response ->
      response.status shouldBe 200
      response.body().price shouldBe 899.99
    }
  }
}

PATCH Requests

Partial updates with PATCH:

TestSystem.validate {
  http {
    // PATCH with response body
    patchAndExpectBody<UserResponse>(
      uri = "/users/123",
      body = mapOf("email" to "newemail@example.com").some()
    ) { response ->
      response.status shouldBe 200
      response.body().email shouldBe "newemail@example.com"
    }
  }
}

DELETE Requests

Delete operations:

TestSystem.validate {
  http {
    // DELETE without response body
    deleteAndExpectBodilessResponse("/users/123") { response ->
      response.status shouldBe 204
    }

    // DELETE with authentication
    deleteAndExpectBodilessResponse(
      uri = "/products/456",
      token = "jwt-token".some()
    ) { response ->
      response.status shouldBe 200
    }
  }
}

File Upload with Multipart

Upload files using multipart form data:

TestSystem.validate {
  http {
    postMultipartAndExpectResponse<UploadResponse>(
      uri = "/products/import",
      body = listOf(
        StoveMultiPartContent.Text("productName", "Laptop"),
        StoveMultiPartContent.Text("description", "A powerful laptop"),
        StoveMultiPartContent.File(
          param = "file",
          fileName = "products.csv",
          content = csvBytes,
          contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE
        )
      )
    ) { response ->
      response.status shouldBe 200
      response.body().uploadedFiles.size shouldBe 1
      response.body().message shouldContain "products.csv"
    }
  }
}

Advanced: Using Ktor Client Directly

For advanced scenarios, access the underlying Ktor HttpClient:

TestSystem.validate {
  http {
    client { baseUrl ->
      // Direct access to Ktor HttpClient
      val response = get {
        url(baseUrl.buildString() + "/custom-endpoint")
        header("Custom-Header", "value")
      }
      println(response.status)
    }
  }
}

Complete Example

Here's a complete CRUD test example:

test("should perform CRUD operations on products") {
  TestSystem.validate {
    var productId: Long? = null

    // CREATE
    http {
      postAndExpectBody<ProductResponse>(
        uri = "/products",
        body = CreateProductRequest(name = "Laptop", price = 999.99, categoryId = 1).some()
      ) { response ->
        response.status shouldBe 201
        productId = response.body().id
        response.body().name shouldBe "Laptop"
      }
    }

    // READ
    http {
      get<ProductResponse>("/products/$productId") { product ->
        product.id shouldBe productId
        product.name shouldBe "Laptop"
        product.price shouldBe 999.99
      }
    }

    // UPDATE
    http {
      putAndExpectJson<ProductResponse>("/products/$productId") {
        UpdateProductRequest(price = 899.99)
      } { product ->
        product.price shouldBe 899.99
      }
    }

    // DELETE
    http {
      deleteAndExpectBodilessResponse("/products/$productId") { response ->
        response.status shouldBe 204
      }
    }

    // Verify deletion
    http {
      getResponse<ErrorResponse>("/products/$productId") { response ->
        response.status shouldBe 404
      }
    }
  }
}

Integration with Other Components

HTTP + Database

TestSystem.validate {
  // Create via API
  val userId = http {
    postAndExpectBody<UserResponse>("/users", body = CreateUserRequest(name = "John").some()) { response ->
      response.body().id
    }
  }

  // Verify in database
  postgresql {
    shouldQuery<User>("SELECT * FROM users WHERE id = $userId") { users ->
      users.size shouldBe 1
      users.first().name shouldBe "John"
    }
  }
}

HTTP + Kafka

TestSystem.validate {
  // Trigger event via API
  http {
    postAndExpectBodilessResponse("/orders", body = CreateOrderRequest(amount = 100.0).some()) { response ->
      response.status shouldBe 201
    }
  }

  // Verify event was published
  kafka {
    shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {
      actual.amount == 100.0
    }
  }
}

HTTP + WireMock

TestSystem.validate {
  // Mock external service
  wiremock {
    mockGet(
      url = "/external-api/data",
      statusCode = 200,
      responseBody = ExternalData(id = 1, value = "test").some()
    )
  }

  // Call your API that depends on external service
  http {
    get<ResponseData>("/data") { response ->
      response.value shouldBe "test"
    }
  }
}

Error Handling

TestSystem.validate {
  http {
    // Test validation errors
    postAndExpectBody<ValidationErrorResponse>("/users", body = InvalidUserRequest().some()) { response ->
      response.status shouldBe 400
      response.body().errors shouldContain "name is required"
    }

    // Test authentication errors
    getResponse<ErrorResponse>("/secure-endpoint") { response ->
      response.status shouldBe 401
    }

    // Test not found
    getResponse<ErrorResponse>("/users/999999") { response ->
      response.status shouldBe 404
    }

    // Test business logic errors
    postAndExpectBody<ErrorResponse>("/products", body = InvalidProductRequest().some()) { response ->
      response.status shouldBe 409 // Conflict
      response.body().message shouldContain "already exists"
    }
  }
}