Self-testing REST APIs
with
API First, Swagger and Play
You need two days to write test cases!?
You already have the Requirements Specification.
Just copy and paste from it.
http://inderpsingh.blogspot.de/2010/05/funny-things-that-testers-hear.html
Specification
Specification
Specification
Specification
Architectural style
Specification
Architectural style
Technological stack
Architectural style
Architectural style
REST
REST
• Client-Server
• Stateless
• Cacheable
• Layered System
• Uniform Interface
• Identification of resources
• Manipulation of resources through these representations
• Self-descriptive messages
• Hypermedia as the engine of application state
Everybody's doing it…
Testers, how are you doing it?
All of them are just different views of
the same piece of software…
What is software?
Simple
the programs that run on a computer and perform certain
functions
something used or associated with and usually contrasted
with hardware as the entire set of programs, procedures,
and related documentation associated with a system and
especially a computer system.
Full
Can you remember quality metrics of your current project?
Libraries it uses?
Runtime environment it runs in?
Container or/and hypervisor?
Operating system?
Firmware/BIOS ?
[A] constructive approach to the problem of program
correctness [is] a usual technique to make a program and
then to test it. But, program testing can be a very effective
way to show the presence of bugs, it is hopelessly
inadequate for showing their absence. The only effective
way to raise the confidence level of a program significantly
is to give a convincing proof of its correctness.
-- Edsger, Wybe Di jkstra, ACM Turing Lecture, The Humble Programmer, 1972
Property based testing
Types are specifications of possible values complying to that Type
Types describe the rules that values must comply to
A possibility to generate ranges of data values for given Types
Boolean: True or False
Equality: Equal or Not Equal
Ordering: Greater, Equal or Less
Provable
Numbers
Strings
Pretty much anything not composed from Provables
Falsifiable
Let’s look again at our system
aka Ports and Adapters
Chris Fidao
https://www.youtube.com/watch?v=6SBjKOwVq0o
Specification
Architectural style
Technology stack
Let’s look again at our solution space
Transport
Transport
Validations
Validations
Model
Model
DRY
DRY
Most people take DRY to mean you shouldn't duplicate code.
That's not its intention. The idea behind DRY is far grander
than that. DRY says that every piece of system knowledge
should have one authoritative, unambiguous representation.
Dave Thomas
Specification is software
It should be a single source of
truth about the system
Formal specification gives us:
Possibility to prove implementation conformance
Self-Evident poorly specified areas
Self-Documented API definition
Intelligent scaffolding
http://www.heppenstall.ca/academics/doc/320/L15_Z.pdf
You need two days to write test cases!?
You already have the requirements specification.
Just copy and paste from it.
Specification
Architectural style
Technology stack
Let’s look again at our technology choice
• Easy to use
• Human readable
• Widest adoption
• Open Source
• Scala and Java
• Dynamic recompilation
• Hot reload
• Asynchronous IO
• Easy to use
Specification
URLs
Verbs
Parameters
Security
Definitions
Specification
URLs
Verbs
Parameters
Security
Definitions Validations
Model
Test Data
Validations
Play Routes
Marshallers
Tests
Controllers
DEMO
Architecture
AST
Play
Akka HTTP
Swagger
RAML
Apiary
Blueprint
…
…
…
Generated code
Metadata
swagger: "2.0"

info:

version: 1.0.0

title: Swagger Petstore

description: A sample API that uses a petstore as an example to
demonstrate features in the swagger-2.0 specification

termsOfService: http://swagger.io/terms/

contact:

name: Swagger API Team

email: foo@example.com

url: http://madskristensen.net

license:

name: MIT

url: http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT

host: petstore.swagger.io

basePath: /api

schemes:

- http

consumes:

- application/json

produces:

- application/json
Definitions
definitions:

Pet:

allOf:

- $ref: '#/definitions/NewPet'

- required:

- id

properties:

id:

type: integer

format: int64

NewPet:

required:

- name 

properties:

name:

type: string

tag:

type: string 

Error:

required:

- code

- message

properties:

code:

type: integer

format: int32

message:

type: string
object definitions {

trait NewPetDef {

def name: String

def tag: Option[String]

}

case class Pet(

id: Option[Long],

name: String,

tag: Option[String]

) extends NewPetDef

case class NewPet(

name: String,

tag: Option[String]

) extends NewPetDef

case class Error(

code: Int,

message: String

)

}
Test data
definitions:

Pet:

allOf:

- $ref: '#/definitions/NewPet'

- required:

- id

properties:

id:

type: integer

format: int64

NewPet:

required:

- name 

properties:

name:

type: string

tag:

type: string 

Error:

required:

- code

- message

properties:

code:

type: integer

format: int32

message:

type: string
object generatorDefinitions {



def createPet = _generate(PetGenerator)

def createNewPet = _generate(NewPetGenerator)

def createError = _generate(ErrorGenerator)

// test data generator for /definitions/Pet

val PetGenerator =

for {

id <- Gen.option(arbitrary[Long])

name <- arbitrary[String]

tag <- Gen.option(arbitrary[String])

} yield Pet(id, name, tag)

// test data generator for /definitions/NewPet

val NewPetGenerator =

for {

name <- arbitrary[String]

tag <- Gen.option(arbitrary[String])

} yield NewPet(name, tag)

// test data generator for /definitions/Error

val ErrorGenerator =

for {

code <- arbitrary[Int]

message <- arbitrary[String]

} yield Error(code, message)

def _generate[T](gen: Gen[T]) = (count: Int) =>
for (i <- 1 to count) yield gen.sample

}
Validations'#/definitions/NewPet'

red:

rties:

ype: integer

ormat: int64



s:

string

string 

ge

s:

integer

t: int32

:

string
class PetValidation(instance: Pet) {

import de.zalando.play.controllers.PlayValidations._

val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]

val result = {

val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)

if (errors.nonEmpty) Left(errors) else Right(instance)

}

}





class NewPetValidation(instance: NewPet) {

import de.zalando.play.controllers.PlayValidations._

val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]

val result = {

val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)

if (errors.nonEmpty) Left(errors) else Right(instance)

}

}





class ErrorValidation(instance: Error) {

import de.zalando.play.controllers.PlayValidations._

val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]

val result = {

val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)

if (errors.nonEmpty) Left(errors) else Right(instance)

}

}
Validations
/pets/{id}:

get:

description: Returns a user based
on a single ID, if the user does not
have access to the pet

operationId: find pet by id

parameters:

- name: id

in: path

description: ID of pet to fetch

required: true

type: integer

format: int64

responses:

200:

description: pet response

schema:

$ref: '#/definitions/Pet'

default:

description: unexpected error

schema:

$ref: '#/definitions/Error'
class ValidationForPetexpandedYamlfindPetById(in: (Long)) {

val (id) = in



val idConstraints = new ValidationBase[Long] {

override def constraints: Seq[Constraint[Long]] = Seq()

}



val normalValidations =
Seq(idConstraints.applyConstraints(id))



val containerValidations =
Seq.empty[scala.Either[scala.Seq[ParsingError], String]]



val rightResult = Right((id))



val allValidations = normalValidations ++
containerValidations



val result = {

val errors =
allValidations.filter(_.isLeft).flatMap(_.left.get)

if (errors.nonEmpty) Left(errors) else rightResult

}

}
Tests
"discard invalid data" in new WithApplication {

val genInputs =

for {

id <- arbitrary[Long]

} yield (id)

val inputs = genInputs suchThat { i => new ValidationForPetexpandedYamlfindPetById(i).result !=
Right(i) }

val props = forAll(inputs) { i => testInvalidInput(i) }

checkResult(props)

}
Tests
def testInvalidInput(in: (Long)) = {

val (id) = in

val url = s"""/api/pets/${id}"""

val path = route(FakeRequest(GET, url)).get

val validation = new ValidationForPetexpandedYamlfindPetById(id).result

lazy val validations = validation.left.get flatMap {

_.messages map { m => contentAsString(path).contains(m) ?= true }

}

("given an URL: [" + url + "]") |: all(

status(path) ?= BAD_REQUEST,

contentType(path) ?= Some("application/json"),

validation.isLeft ?= true,

all(validations:_*)

)

}
Controllers
private val findPetByIdResponseMimeType = "application/json"

private val findPetByIdActionSuccessStatus = Status(200)



private type findPetByIdActionRequestType = (Long)

private type findPetByIdActionResultType = Pet

private type findPetByIdActionType = findPetByIdActionRequestType => Either[Throwable, findPetByIdActionResultType]



private def errorToStatusfindPetById: PartialFunction[Throwable, Status] = PartialFunction.empty[Throwable, Status]



def findPetByIdAction = (f: findPetByIdActionType) => (id: Long) => Action {

val result = new ValidationForPetexpandedYamlfindPetById(id).result.right.map {

processValidfindPetByIdRequest(f)

}

implicit val marshaller = parsingErrors2Writable(findPetByIdResponseMimeType)

val response = result.left.map { BadRequest(_) }

response.fold(a => a, c => c)

}



private def processValidfindPetByIdRequest(f: findPetByIdActionType)(request: findPetByIdActionRequestType) = {

val callerResult = f(request)

val status = callerResult match {

case Left(error) => (errorToStatusfindPetById orElse defaultErrorMapping)(error)

case Right(result) => findPetByIdActionSuccessStatus

}

implicit val findPetByIdWritableJson = anyToWritable[findPetByIdActionResultType](findPetByIdResponseMimeType)

status(callerResult)

}
Skeletons
class PetexpandedYaml extends PetexpandedYamlBase {



// handler for GET /pets

def findPets = findPetsAction { in : (Option[Seq[String]], Option[Int]) =>

val (tags, limit) = in

???

}



// handler for POST /pets

def addPet = addPetAction { in : (NewPet) =>

val (pet) = in

???

}



// handler for GET /pets/{id}

def findPetById = findPetByIdAction { in : (Long) =>

val (id) = in

???

}



// handler for DELETE /pets/{id}

def deletePet = deletePetAction { in : (Long) =>

val (id) = in

???

}

}
http://github.com/zalando/play-swagger
Questions?

QA Lab: тестирование ПО. Станислав Шмидт: "Self-testing REST APIs with API First, Swagger and Play"

  • 1.
    Self-testing REST APIs with APIFirst, Swagger and Play
  • 2.
    You need twodays to write test cases!? You already have the Requirements Specification. Just copy and paste from it. http://inderpsingh.blogspot.de/2010/05/funny-things-that-testers-hear.html
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
    REST • Client-Server • Stateless •Cacheable • Layered System • Uniform Interface • Identification of resources • Manipulation of resources through these representations • Self-descriptive messages • Hypermedia as the engine of application state
  • 11.
  • 15.
    Testers, how areyou doing it?
  • 17.
    All of themare just different views of the same piece of software…
  • 18.
  • 19.
    Simple the programs thatrun on a computer and perform certain functions
  • 20.
    something used orassociated with and usually contrasted with hardware as the entire set of programs, procedures, and related documentation associated with a system and especially a computer system. Full
  • 21.
    Can you rememberquality metrics of your current project? Libraries it uses? Runtime environment it runs in? Container or/and hypervisor? Operating system? Firmware/BIOS ?
  • 22.
    [A] constructive approachto the problem of program correctness [is] a usual technique to make a program and then to test it. But, program testing can be a very effective way to show the presence of bugs, it is hopelessly inadequate for showing their absence. The only effective way to raise the confidence level of a program significantly is to give a convincing proof of its correctness. -- Edsger, Wybe Di jkstra, ACM Turing Lecture, The Humble Programmer, 1972
  • 23.
  • 24.
    Types are specificationsof possible values complying to that Type Types describe the rules that values must comply to A possibility to generate ranges of data values for given Types
  • 25.
    Boolean: True orFalse Equality: Equal or Not Equal Ordering: Greater, Equal or Less Provable
  • 26.
    Numbers Strings Pretty much anythingnot composed from Provables Falsifiable
  • 27.
    Let’s look againat our system
  • 29.
    aka Ports andAdapters Chris Fidao https://www.youtube.com/watch?v=6SBjKOwVq0o
  • 30.
  • 31.
  • 32.
  • 33.
    DRY Most people takeDRY to mean you shouldn't duplicate code. That's not its intention. The idea behind DRY is far grander than that. DRY says that every piece of system knowledge should have one authoritative, unambiguous representation. Dave Thomas
  • 34.
    Specification is software Itshould be a single source of truth about the system
  • 35.
    Formal specification givesus: Possibility to prove implementation conformance Self-Evident poorly specified areas Self-Documented API definition Intelligent scaffolding http://www.heppenstall.ca/academics/doc/320/L15_Z.pdf
  • 36.
    You need twodays to write test cases!? You already have the requirements specification. Just copy and paste from it.
  • 37.
  • 38.
    • Easy touse • Human readable • Widest adoption • Open Source • Scala and Java • Dynamic recompilation • Hot reload • Asynchronous IO • Easy to use
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
    Metadata swagger: "2.0"
 info:
 version: 1.0.0
 title:Swagger Petstore
 description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification
 termsOfService: http://swagger.io/terms/
 contact:
 name: Swagger API Team
 email: [email protected]
 url: http://madskristensen.net
 license:
 name: MIT
 url: http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
 host: petstore.swagger.io
 basePath: /api
 schemes:
 - http
 consumes:
 - application/json
 produces:
 - application/json
  • 46.
    Definitions definitions:
 Pet:
 allOf:
 - $ref: '#/definitions/NewPet'
 -required:
 - id
 properties:
 id:
 type: integer
 format: int64
 NewPet:
 required:
 - name 
 properties:
 name:
 type: string
 tag:
 type: string 
 Error:
 required:
 - code
 - message
 properties:
 code:
 type: integer
 format: int32
 message:
 type: string object definitions {
 trait NewPetDef {
 def name: String
 def tag: Option[String]
 }
 case class Pet(
 id: Option[Long],
 name: String,
 tag: Option[String]
 ) extends NewPetDef
 case class NewPet(
 name: String,
 tag: Option[String]
 ) extends NewPetDef
 case class Error(
 code: Int,
 message: String
 )
 }
  • 47.
    Test data definitions:
 Pet:
 allOf:
 - $ref:'#/definitions/NewPet'
 - required:
 - id
 properties:
 id:
 type: integer
 format: int64
 NewPet:
 required:
 - name 
 properties:
 name:
 type: string
 tag:
 type: string 
 Error:
 required:
 - code
 - message
 properties:
 code:
 type: integer
 format: int32
 message:
 type: string object generatorDefinitions {
 
 def createPet = _generate(PetGenerator)
 def createNewPet = _generate(NewPetGenerator)
 def createError = _generate(ErrorGenerator)
 // test data generator for /definitions/Pet
 val PetGenerator =
 for {
 id <- Gen.option(arbitrary[Long])
 name <- arbitrary[String]
 tag <- Gen.option(arbitrary[String])
 } yield Pet(id, name, tag)
 // test data generator for /definitions/NewPet
 val NewPetGenerator =
 for {
 name <- arbitrary[String]
 tag <- Gen.option(arbitrary[String])
 } yield NewPet(name, tag)
 // test data generator for /definitions/Error
 val ErrorGenerator =
 for {
 code <- arbitrary[Int]
 message <- arbitrary[String]
 } yield Error(code, message)
 def _generate[T](gen: Gen[T]) = (count: Int) => for (i <- 1 to count) yield gen.sample
 }
  • 48.
    Validations'#/definitions/NewPet'
 red:
 rties:
 ype: integer
 ormat: int64
 
 s:
 string
 string
 ge
 s:
 integer
 t: int32
 :
 string class PetValidation(instance: Pet) {
 import de.zalando.play.controllers.PlayValidations._
 val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
 val result = {
 val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)
 if (errors.nonEmpty) Left(errors) else Right(instance)
 }
 }
 
 
 class NewPetValidation(instance: NewPet) {
 import de.zalando.play.controllers.PlayValidations._
 val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
 val result = {
 val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)
 if (errors.nonEmpty) Left(errors) else Right(instance)
 }
 }
 
 
 class ErrorValidation(instance: Error) {
 import de.zalando.play.controllers.PlayValidations._
 val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
 val result = {
 val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)
 if (errors.nonEmpty) Left(errors) else Right(instance)
 }
 }
  • 49.
    Validations /pets/{id}:
 get:
 description: Returns auser based on a single ID, if the user does not have access to the pet
 operationId: find pet by id
 parameters:
 - name: id
 in: path
 description: ID of pet to fetch
 required: true
 type: integer
 format: int64
 responses:
 200:
 description: pet response
 schema:
 $ref: '#/definitions/Pet'
 default:
 description: unexpected error
 schema:
 $ref: '#/definitions/Error' class ValidationForPetexpandedYamlfindPetById(in: (Long)) {
 val (id) = in
 
 val idConstraints = new ValidationBase[Long] {
 override def constraints: Seq[Constraint[Long]] = Seq()
 }
 
 val normalValidations = Seq(idConstraints.applyConstraints(id))
 
 val containerValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
 
 val rightResult = Right((id))
 
 val allValidations = normalValidations ++ containerValidations
 
 val result = {
 val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)
 if (errors.nonEmpty) Left(errors) else rightResult
 }
 }
  • 50.
    Tests "discard invalid data"in new WithApplication {
 val genInputs =
 for {
 id <- arbitrary[Long]
 } yield (id)
 val inputs = genInputs suchThat { i => new ValidationForPetexpandedYamlfindPetById(i).result != Right(i) }
 val props = forAll(inputs) { i => testInvalidInput(i) }
 checkResult(props)
 }
  • 51.
    Tests def testInvalidInput(in: (Long))= {
 val (id) = in
 val url = s"""/api/pets/${id}"""
 val path = route(FakeRequest(GET, url)).get
 val validation = new ValidationForPetexpandedYamlfindPetById(id).result
 lazy val validations = validation.left.get flatMap {
 _.messages map { m => contentAsString(path).contains(m) ?= true }
 }
 ("given an URL: [" + url + "]") |: all(
 status(path) ?= BAD_REQUEST,
 contentType(path) ?= Some("application/json"),
 validation.isLeft ?= true,
 all(validations:_*)
 )
 }
  • 52.
    Controllers private val findPetByIdResponseMimeType= "application/json"
 private val findPetByIdActionSuccessStatus = Status(200)
 
 private type findPetByIdActionRequestType = (Long)
 private type findPetByIdActionResultType = Pet
 private type findPetByIdActionType = findPetByIdActionRequestType => Either[Throwable, findPetByIdActionResultType]
 
 private def errorToStatusfindPetById: PartialFunction[Throwable, Status] = PartialFunction.empty[Throwable, Status]
 
 def findPetByIdAction = (f: findPetByIdActionType) => (id: Long) => Action {
 val result = new ValidationForPetexpandedYamlfindPetById(id).result.right.map {
 processValidfindPetByIdRequest(f)
 }
 implicit val marshaller = parsingErrors2Writable(findPetByIdResponseMimeType)
 val response = result.left.map { BadRequest(_) }
 response.fold(a => a, c => c)
 }
 
 private def processValidfindPetByIdRequest(f: findPetByIdActionType)(request: findPetByIdActionRequestType) = {
 val callerResult = f(request)
 val status = callerResult match {
 case Left(error) => (errorToStatusfindPetById orElse defaultErrorMapping)(error)
 case Right(result) => findPetByIdActionSuccessStatus
 }
 implicit val findPetByIdWritableJson = anyToWritable[findPetByIdActionResultType](findPetByIdResponseMimeType)
 status(callerResult)
 }
  • 53.
    Skeletons class PetexpandedYaml extendsPetexpandedYamlBase {
 
 // handler for GET /pets
 def findPets = findPetsAction { in : (Option[Seq[String]], Option[Int]) =>
 val (tags, limit) = in
 ???
 }
 
 // handler for POST /pets
 def addPet = addPetAction { in : (NewPet) =>
 val (pet) = in
 ???
 }
 
 // handler for GET /pets/{id}
 def findPetById = findPetByIdAction { in : (Long) =>
 val (id) = in
 ???
 }
 
 // handler for DELETE /pets/{id}
 def deletePet = deletePetAction { in : (Long) =>
 val (id) = in
 ???
 }
 }
  • 54.
  • 55.