Intro to Play Framework – Scala Edition – Testing



Intro to Play Framework – Scala Edition – Testing

0 4


introduction_to_the_play_framework-scala


On Github jamesward / introduction_to_the_play_framework-scala

Intro to Play Framework

Scala Edition

James Ward ~ @_JamesWard

Play Framework

The High Velocity Web Framework for Java & Scala

  • Just hit refresh workflow
  • Type safety
  • RESTful by default
  • Stateless
  • Reactive
  • Asset Pipeline
  • First-class JSON

Get Started with Typesafe Activator

typesafe.com/platform/getstarted

  • 150+ Templates with Tutorials
  • Web UI and CLI
  • Edit Code in Browser, Eclipse, IntelliJ IDEA, or other editor
  • Run App & Tests via Browser
  • Typesafe Console Integration

Create a New Play App

activator new

IDE Support

activator idea

Run the App

activator ~run

Open the App

http://localhost:9000

Routes

Declarative, Type-safe URL Mapping

VERB    PATH                CONTROLLER_METHOD

GET     /                   controllers.Application.index()
GET     /foo                controllers.Application.foo()

Controllers

Receive a Request, Return a Response

def index = Action {
  Ok(index.render("Your new application is ready."))
}

Views

Composable, Type-safe Scala Templates

@(message: String)
@main("Welcome to Play 2.0") {
    @message
}

Testing

Specs2, ScalaTest, etc

Run All Tests Once

activator test

Run All Tests Continuously

activator ~test

Run One Test

activator "test-only my.namespace.MySpec"

Run Failed Tests

activator test-quick

Test a Template

"render index template" in new WithApplication {
    val html = views.html.index("Coco")
    contentAsString(html) must contain("Hello Coco")
}

Test a Controller

object FunctionalExampleControllerSpec extends PlaySpecification {

    "respond to the index Action" in new WithApplication {
        val result = controllers.Application.index()(FakeRequest())

        status(result) must equalTo(OK)
        contentType(result) must beSome("text/plain")
        contentAsString(result) must contain("Hello Bob")
    }
}

Test a Route

"respond to the index Action" in new WithApplication {
    val Some(result) = route(FakeRequest(GET, "/Bob"))

    status(result) must equalTo(OK)
    contentType(result) must beSome("text/html")
    charset(result) must beSome("utf-8")
    contentAsString(result) must contain("Hello Bob")
}

JSON

Very Rich Scala JSON API

  • Reflection-free via Macros
  • Built-in Validators
  • JSON Coast-to-Coast
  • Interpolation & Pattern Matching

Reflection-free via Macros

case class Foo(name: String)

object Foo {
    implicit val fooFormat = Json.format[Foo]
}

Returning JSON

def foo = Action {
    Ok(Foo("asdf"))
}

Built-in Validators

object Foo {
    implicit val fooWrites = Json.writes[Foo]
    implicit val fooReads: Reads[Foo] = (__ \ "name").read[String](minLength[String](5)).map(Foo(_))
}

Json.parse("""{"name": "asdf"}""").validate[Foo]
// JsError(List((/name,List(ValidationError(error.minLength,WrappedArray(5))))))

JSON Coast-to-Coast

val foo = Json.obj("name" -> "Bob")
val transformer = (__ \ "name").json.put(JsString("foo"))
val newFoo = foo.transform(transformer)
newFoo.map(Ok(_)).getOrElse(BadRequest)

Interpolation & Pattern Matching

JsZipper: https://github.com/mandubian/play-json-zipper

val js = json"""{ "foo" : "bar", "foo2" : 123 }"""

js match {
    case json"""{ "foo" : $a, "foo2" : $b }""" => Some(a -> b)
    case _ => None
}

Asset Pipeline via sbt-web

Unified Client/Server Development

  • Incremental
  • Parallel
  • Node native or Node JVM (Trireme)
  • Dependency management with WebJars and/or NPM
  • Any sbt project
  • sbt-autoprefixer
  • sbt-closure
  • sbt-coffeescript
  • sbt-concat
  • sbt-css-compress
  • sbt-digest
  • sbt-dustjs-linkedin
  • sbt-filter
  • sbt-gzip
  • sbt-html-minifier
  • sbt-imagemin
  • sbt-jshint
  • sbt-jst
  • sbt-less
  • sbt-mocha
  • sbt-purescript
  • sbt-rjs
  • sbt-simple-url-update
  • sbt-stylus
  • sbt-uglify

Setup

project/plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0")
assets/javascripts/foo.coffee
<script src='@routes.Assets.at("javascripts/foo.min.js")'></script>

WebJars

The Web in Jars

  • Common client-side libraries: jQuery, Bootstrap, etc
  • Manage versioning through dependency management
  • Transitive dependencies
  • RequireJS support
  • CDN & Cache Friendly
Specify Dependencies in build.sbt
"org.webjars" % "jquery" % "2.1.1"
Use a WebJar
<script src='@routes.Assets.at("lib/jquery/jquery.min.js)'></script>

Reactive Requests

Blocking Request

def foo = Action {
    Ok("foo")
}

Async Request

def foo = Action.async {
    Future(Ok("foo"))
}

Reactive Request (Async + Non-Blocking)

def pause(duration: Int) = Action.async {
    Promise.timeout(Ok(duration.toString), duration seconds)
}

Reactive WS Client

val futureResponse: Future[Response] = WS.url("http://www.foo.com").get

Reactive Composition

def foo = Action.async {
    val futureResponse = WS.url("http://www.foo.com").get
    futureResponse.map { response =>
        Ok(response.body)
    }
}

Reactive Composition

def foo = Action.async {
    val futureJW = WS.url("http://www.jamesward.com").get
    val futureTwitter = WS.url("http://www.twitter.com").get
    for {
        jw <- futureJW
        twitter <- futureTwitter
    } yield Ok(jw.response.body + twitter.response.body)
}

Reactive Push with SSE

def events = Action {
    val (enumerator, channel) = Concurrent.broadcast[String]

    Akka.system.scheduler.schedule(Duration.Zero, 1.second, new Runnable {
        def run() = channel.push("hello")
    })

    Ok.feed(enumerator &> EventSource()).as(EVENT_STREAM)
}

$ ->
  events = new EventSource("/events")
  events.onmessage = (e) ->
    console.log(e.data)

2-Way Reactive with WebSockets

def echoWs = WebSocket.using[String] { request =>
    val (enumerator, channel) = Concurrent.broadcast[String]
    val in = Iteratee.foreach[String](channel.push)
    (in, enumerator)
}

$ ->
    ws = new WebSocket("ws://localhost:9000/echo")
    ws.onopen = () ->
        ws.send("foo")
    ws.onmessage = (message) ->
        console.log(message.data)