Asynchronous HTTP client for Play Framework with OAuth, OpenID, and SSL/TLS support
—
Testing utilities for HTTP client operations in Play applications. Play WS provides comprehensive testing support for both unit tests and integration tests with HTTP clients.
Main testing interface for HTTP client operations.
/**
* Test client trait providing HTTP testing utilities
*/
trait WsTestClient {
/**
* Create a WSRequest for a Play Call (route)
* @param call Play framework Call (route definition)
* @param port Implicit test server port
* @param client Implicit WSClient instance
* @return WSRequest configured for the test server
*/
def wsCall(call: Call)(implicit port: Port, client: WSClient): WSRequest
/**
* Create a WSRequest for a URL relative to test server
* @param url URL path (will be prefixed with test server base URL)
* @param port Implicit test server port
* @param client Implicit WSClient instance
* @return WSRequest configured for the test server
*/
def wsUrl(url: String)(implicit port: Port, client: WSClient): WSRequest
/**
* Execute test block with a temporary WSClient
* @param block Test code block that uses WSClient
* @param port Implicit test server port
* @return Result of executing the test block
*/
def withClient[T](block: WSClient => T)(implicit port: Port): T
}
/**
* WS test client object implementing the trait
*/
object WsTestClient extends WsTestClientTesting with Play Test Server:
import play.api.test._
import play.api.test.Helpers._
import play.api.libs.ws._
import org.scalatestplus.play._
class MyControllerSpec extends PlaySpec with OneServerPerSuite {
"MyController" should {
"return JSON data" in {
implicit val client = app.injector.instanceOf[WSClient]
val response = await(WsTestClient.wsUrl("/api/users").get())
response.status mustBe OK
response.header("Content-Type") must contain("application/json")
val json = response.json
(json \ "users").as[Seq[JsObject]] must not be empty
}
}
}Testing with Temporary Client:
import play.api.test._
import play.api.test.Helpers._
class HttpClientSpec extends PlaySpec with OneServerPerSuite {
"HTTP client" should {
"handle external API calls" in {
WsTestClient.withClient { client =>
val response = await(client.url("https://api.example.com/test").get())
response.status mustBe OK
}
}
}
}Test your application routes using the wsCall method.
import play.api.routing.sird._
import play.api.test._
import play.api.test.Helpers._
import play.api.mvc._
class RouteTestSpec extends PlaySpec with OneServerPerSuite {
override def fakeApplication(): Application = {
new GuiceApplicationBuilder()
.router(Router.from {
case GET(p"/api/hello") => Action {
Ok(Json.obj("message" -> "Hello World"))
}
case POST(p"/api/echo") => Action(parse.json) { request =>
Ok(request.body)
}
})
.build()
}
"API routes" should {
"handle GET request" in {
implicit val client = app.injector.instanceOf[WSClient]
// Test using route call
val call = controllers.routes.ApiController.hello() // Assuming you have this route
val response = await(WsTestClient.wsCall(call).get())
response.status mustBe OK
(response.json \ "message").as[String] mustBe "Hello World"
}
"handle POST request with JSON" in {
implicit val client = app.injector.instanceOf[WSClient]
val testData = Json.obj("name" -> "John", "age" -> 30)
val response = await(WsTestClient.wsUrl("/api/echo").post(testData))
response.status mustBe OK
response.json mustEqual testData
}
}
}Create mock servers for testing external API interactions.
import play.core.server.Server
import play.api.routing.sird._
import play.api.mvc._
import play.api.test._
import scala.concurrent.ExecutionContext.Implicits.global
class ExternalApiSpec extends PlaySpec {
"External API client" should {
"handle mock server responses" in {
// Create mock server
Server.withRouter() {
case GET(p"/api/users/$id") => Action {
Ok(Json.obj(
"id" -> id,
"name" -> s"User $id",
"email" -> s"user$id@example.com"
))
}
case POST(p"/api/users") => Action(parse.json) { request =>
val name = (request.body \ "name").as[String]
Created(Json.obj(
"id" -> 123,
"name" -> name,
"email" -> s"${name.toLowerCase}@example.com"
))
}
} { implicit port =>
WsTestClient.withClient { client =>
// Test GET request
val getResponse = await(client.url(s"http://localhost:$port/api/users/1").get())
getResponse.status mustBe OK
(getResponse.json \ "name").as[String] mustBe "User 1"
// Test POST request
val postData = Json.obj("name" -> "Alice")
val postResponse = await(client.url(s"http://localhost:$port/api/users").post(postData))
postResponse.status mustBe CREATED
(postResponse.json \ "id").as[Int] mustBe 123
}
}
}
}
}Test OAuth and other authentication mechanisms.
import play.api.test._
import play.api.libs.oauth._
class OAuthTestSpec extends PlaySpec with OneServerPerSuite {
"OAuth integration" should {
"handle OAuth flow" in {
implicit val client = app.injector.instanceOf[WSClient]
// Mock OAuth service info
val serviceInfo = ServiceInfo(
requestTokenURL = s"http://localhost:$port/oauth/request_token",
accessTokenURL = s"http://localhost:$port/oauth/access_token",
authorizationURL = s"http://localhost:$port/oauth/authorize",
key = ConsumerKey("test_key", "test_secret")
)
val oauth = OAuth(serviceInfo)
// Test would require mock OAuth endpoints
// This is a simplified example structure
oauth.retrieveRequestToken("http://callback") match {
case Right(token) =>
token.token must not be empty
case Left(error) =>
fail(s"OAuth request failed: $error")
}
}
}
}Test error conditions and edge cases.
class ErrorHandlingSpec extends PlaySpec with OneServerPerSuite {
"Error handling" should {
"handle 404 responses" in {
implicit val client = app.injector.instanceOf[WSClient]
val response = await(WsTestClient.wsUrl("/nonexistent").get())
response.status mustBe NOT_FOUND
}
"handle connection timeouts" in {
WsTestClient.withClient { client =>
val request = client.url("http://10.255.255.1:12345/timeout")
.withRequestTimeout(1000) // 1 second timeout
// This should timeout quickly
intercept[java.util.concurrent.TimeoutException] {
await(request.get())
}
}
}
"handle invalid JSON responses gracefully" in {
Server.withRouter() {
case GET(p"/invalid-json") => Action {
Ok("{ invalid json }")
}
} { implicit port =>
WsTestClient.withClient { client =>
val response = await(client.url(s"http://localhost:$port/invalid-json").get())
response.status mustBe OK
intercept[com.fasterxml.jackson.core.JsonParseException] {
response.json
}
}
}
}
}
}Test streaming responses and large data handling.
import play.api.libs.iteratee._
import akka.util.ByteString
class StreamingSpec extends PlaySpec with OneServerPerSuite {
"Streaming responses" should {
"handle large responses" in {
Server.withRouter() {
case GET(p"/large-data") => Action {
val largeData = "x" * 1000000 // 1MB of data
Ok(largeData)
}
} { implicit port =>
WsTestClient.withClient { client =>
val response = await(client.url(s"http://localhost:$port/large-data").stream())
response match {
case (headers, body) =>
headers.status mustBe OK
// Consume the stream
val consumed = body |>>> Iteratee.consume[Array[Byte]]()
val data = await(consumed)
data.length mustBe 1000000
}
}
}
}
}
}Create custom WS client configurations for testing.
import play.api.libs.ws.ning._
import scala.concurrent.duration._
class CustomConfigSpec extends PlaySpec {
"Custom WS configuration" should {
"use test-specific timeouts" in {
val testConfig = NingWSClientConfig(
wsClientConfig = WSClientConfig(
connectionTimeout = 5.seconds,
requestTimeout = 10.seconds,
followRedirects = false
)
)
val client = NingWSClient(testConfig)
try {
// Use client for testing with custom configuration
val response = await(client.url("https://httpbin.org/delay/2").get())
response.status mustBe OK
} finally {
client.close()
}
}
}
}Complete setup for integration testing with WS client.
import org.scalatestplus.play._
import play.api.test._
import play.api.inject.guice.GuiceApplicationBuilder
class IntegrationTestSpec extends PlaySpec with OneServerPerSuite with OneBrowserPerSuite {
override def fakeApplication(): Application = {
new GuiceApplicationBuilder()
.configure(
"play.ws.timeout.connection" -> "10s",
"play.ws.timeout.idle" -> "10s",
"play.ws.timeout.request" -> "10s"
)
.build()
}
"Full application" should {
"handle complete user workflow" in {
implicit val client = app.injector.instanceOf[WSClient]
// Step 1: Create user
val createUser = Json.obj(
"name" -> "Test User",
"email" -> "test@example.com"
)
val createResponse = await(WsTestClient.wsUrl("/api/users").post(createUser))
createResponse.status mustBe CREATED
val userId = (createResponse.json \ "id").as[Long]
// Step 2: Retrieve user
val getResponse = await(WsTestClient.wsUrl(s"/api/users/$userId").get())
getResponse.status mustBe OK
val user = getResponse.json
(user \ "name").as[String] mustBe "Test User"
(user \ "email").as[String] mustBe "test@example.com"
// Step 3: Update user
val updateData = Json.obj("name" -> "Updated User")
val updateResponse = await(WsTestClient.wsUrl(s"/api/users/$userId").put(updateData))
updateResponse.status mustBe OK
// Step 4: Verify update
val verifyResponse = await(WsTestClient.wsUrl(s"/api/users/$userId").get())
(verifyResponse.json \ "name").as[String] mustBe "Updated User"
// Step 5: Delete user
val deleteResponse = await(WsTestClient.wsUrl(s"/api/users/$userId").delete())
deleteResponse.status mustBe NO_CONTENT
}
}
}Basic performance testing with WS client.
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
class PerformanceSpec extends PlaySpec with OneServerPerSuite {
"Performance tests" should {
"handle concurrent requests" in {
implicit val client = app.injector.instanceOf[WSClient]
val concurrentRequests = 10
val requests = (1 to concurrentRequests).map { i =>
WsTestClient.wsUrl(s"/api/users/$i").get()
}
val startTime = System.currentTimeMillis()
val responses = await(Future.sequence(requests))
val endTime = System.currentTimeMillis()
responses.length mustBe concurrentRequests
responses.foreach(_.status mustBe OK)
val totalTime = endTime - startTime
println(s"$concurrentRequests requests completed in ${totalTime}ms")
// Assert reasonable performance (adjust threshold as needed)
totalTime must be < 5000L // Less than 5 seconds
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-typesafe-play--play-ws-2-10