Skip to content
This repository has been archived by the owner on Mar 12, 2020. It is now read-only.

Commit

Permalink
Initial REST service with unit test #1
Browse files Browse the repository at this point in the history
  • Loading branch information
tdauth committed Oct 23, 2018
1 parent c49a4c4 commit decad8c
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 89 deletions.
39 changes: 8 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,44 +55,21 @@ Each transition is a UI action.
A state is defined by the set of all visible and interactable windows together with their enabled widgets.

## REST API
Some suggestions on how the REST API could look like.
Some suggestions on how the REST API could look like:

state/create
Parameter:
List(windows):
- window:
- id
- parent
- List(InteractableComponent):
-- label

action/create
Parameter:
- state from
- state to
There is a special ID for the unknown state.

action/update
- state to

action/delete
Infeasible actions must be deleted before their actual execution.

Edges to the unknown state might lead to another state after some tests.

Simplified REST API for automatic tests which discover new states and transitions:
state/add
Parameter:
- State-Parameter
- Source State

Automatically, a transition to the unknown state.
* `/applications` GET queries all registered GUI applications
* `/create-application` POST registers a new GUI application
* `/application/<long>` GET queries a registered GUI application
* `/application/<long>/test-suites` GET queries all test suites for an existing GUI application
* `/application/<long>/create-test-suite` POST registers a new test suite for an existing GUI application
* `/application/<long>/test-suite/<long>` GET queries a registered test suite for an existing GUI application

## APIs suggested in the project proposal of Surili
* [Selenium WebDriver](http://seleniumhq.org/docs/03_webdriver.jsp)
* [Gherkin](https://github.com/cucumber/cucumber/wiki/Gherkin)

## REST Frameworks for Scala
* Good example of a REST service with Akka: <https://github.com/ArchDev/akka-http-rest>
* <https://doc.akka.io/docs/akka-http/current/>
* <https://www.playframework.com/>
* <https://www.reddit.com/r/scala/comments/6izqac/akka_http_vs_play_ws_what_is_the_current_state/>
Expand Down
3 changes: 3 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ organization := "tdauth"
scalaVersion := "2.12.7"

libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.5"
libraryDependencies += "com.typesafe.akka" %% "akka-http-core" % "10.1.5"
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.5.12"
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.5"
libraryDependencies += "com.typesafe.akka" %% "akka-http-xml" % "10.1.5"
libraryDependencies += "com.typesafe.akka" %% "akka-http-testkit" % "10.1.5" % "test"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test"

Expand Down
132 changes: 132 additions & 0 deletions src/main/scala/de/retest/guistatemachine/RestService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package de.retest.guistatemachine

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import akka.Done
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._

import java.util.LinkedList

// domain model
// TODO Id should use Long and the REST paths as well. Use concurrent hash maps with the IDs and generate new IDs for new items.
final case class Id(id: Int)
final case class GuiApplication() {
val testSuites = TestSuites()
}
final case class GuiApplications() {
val applications = new LinkedList[GuiApplication]
}
final case class TestSuite()
final case class TestSuites() {
val testSuites = new LinkedList[TestSuite]
}

trait RestService {
implicit val system: ActorSystem
implicit val materializer: ActorMaterializer

// database
val guiApplications = GuiApplications()

def addApplication(): Id = {
val apps = guiApplications.applications
apps.synchronized {
apps.add(new GuiApplication)
Id(apps.size() - 1)
}
}
def getApplication(id: Id): Option[GuiApplication] = {
val apps = guiApplications.applications
apps.synchronized {
val index = id.id
if (index >= 0 && index < apps.size()) Some(apps.get(index)) else None
}
}

def getTestSuites(applicationId: Id): Option[TestSuites] = {
val app = getApplication(applicationId)
app match {
case Some(x) => Some(x.testSuites)
case None => None
}
}

def addTestSuite(applicationId: Id): Option[Id] = {
val app = getApplication(applicationId)
app match {
case Some(x) => {
val testSuites = x.testSuites.testSuites
testSuites.synchronized {
testSuites.add(new TestSuite)
Some(Id(testSuites.size() - 1))
}
}
case None => None
}
}
def getTestSuite(applicationId: Id, testSuiteId: Id): Option[TestSuite] = {
val app = getApplication(applicationId)
app match {
case Some(x) => {
val testSuites = x.testSuites.testSuites
testSuites.synchronized {
val index = testSuiteId.id
if (index >= 0 && index < testSuites.size()) Some(testSuites.get(index)) else None
}
}
case None => None
}
}

// formats for unmarshalling and marshalling
implicit val idFormat = jsonFormat1(Id)
implicit val applicationFormat = jsonFormat0(GuiApplication)
implicit val applicationsFormat = jsonFormat0(GuiApplications)
implicit val testSuiteFormat = jsonFormat0(TestSuite)
implicit val testSuitesFormat = jsonFormat0(TestSuites)

val route: Route =
get {
path("applications") {
complete(guiApplications)
} ~
pathPrefix("application" / IntNumber) { id =>
val app = getApplication(Id(id))
app match {
case Some(x) => complete(x)
case None => complete(StatusCodes.NotFound)
}
} ~
pathPrefix("application" / IntNumber / "test-suites") { id =>
val testSuites = getTestSuites(Id(id))
testSuites match {
case Some(x) => complete(x)
case None => complete(StatusCodes.NotFound)
}
} ~
pathPrefix("application" / IntNumber / "test-suite" / IntNumber) { (appId, suiteId) =>
val suite = getTestSuite(Id(appId), Id(suiteId))
suite match {
case Some(x) => complete(x)
case None => complete(StatusCodes.NotFound)
}
}
} ~
post {
path("create-application") {
val id = addApplication()
complete(id)
} ~
pathPrefix("application" / IntNumber / "create-test-suite") { appId =>
{
val id = addTestSuite(Id(appId))
complete(id)
}
}
}
}
10 changes: 2 additions & 8 deletions src/main/scala/de/retest/guistatemachine/WebServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,17 @@ import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import scala.io.StdIn

object WebServer extends App {
object WebServer extends App with RestService {
implicit val system = ActorSystem("gui-state-machine-api-system")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher

val bindingFuture = Http().bindAndHandle(getRoute, "localhost", 8080)
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)

println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
StdIn.readLine() // let it run until user presses return
bindingFuture
.flatMap(_.unbind()) // trigger unbinding from the port
.onComplete(_ => system.terminate()) // and shutdown when done

def getRoute = path("hello") {
get {
complete("Hello!")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package de.retest.guistatemachine.model

/**
* The tested GUI application with an initial state and a number of test suites.
*
* @param id This ID is for the REST API only.
*/
class GuiApplication(initialState: State, testSuites: Seq[TestSuite]) {
class GuiApplication(val id : Long, initialState: State, testSuites: Seq[TestSuite]) {

def getTestSuites: Seq[TestSuite] = testSuites

Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/de/retest/guistatemachine/model/State.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package de.retest.guistatemachine.model

trait State {
class State {
private val windows = Set[GuiWindow]()
/**
* Actions which can be executed by the user in this state.
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/de/retest/guistatemachine/model/TestCase.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package de.retest.guistatemachine.model

class TestCase(app: GuiApplication) {
class TestCase(initialState: State) {
private val actions = Seq[UIAction]()

def length = actions.size

def isValid = true // it is valid if all GUI actions can be executed

def getUiPath = new UIPath(new PathState(app.getInitialState)) // TODO generate the correct path, with the common initial state
def getUiPath = new UIPath(new PathState(initialState)) // TODO generate the correct path, with the common initial state
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package de.retest.guistatemachine.model

class TestSuite(app : GuiApplication) {
class TestSuite {
private val cases = Set[TestCase]()

def size = cases.size
Expand Down
68 changes: 68 additions & 0 deletions src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package de.retest.guistatemachine

import org.scalatest.{ Matchers, WordSpec }
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.server._
import Directives._
import akka.http.scaladsl.model.HttpEntity
import akka.http.scaladsl.model.ContentTypes
import akka.http.scaladsl.model.HttpCharset
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import akka.Done
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._

class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest with RestService {

lazy val sut = route

"The service" should {
"return an empty list for the GET request with the path /applications" in {
Get("/applications") ~> sut ~> check {
val r = responseAs[GuiApplications]
r.applications.size shouldEqual 0
}
}

"allow POST for path /create-application" in {
Post("/create-application") ~> sut ~> check {
responseAs[Id] shouldEqual Id(0)
}
}

"return an empty application for the GET request with the path /application/0" in {
Get("/applications/0") ~> sut ~> check {
val r = responseAs[GuiApplication]
r.testSuites.testSuites.size shouldEqual 0
}
}

"return an empty list for the GET request with the path /application/0/test-suites" in {
Get("/applications/0/test-suites") ~> sut ~> check {
val r = responseAs[TestSuites]
r.testSuites.size shouldEqual 0
}
}

"allow POST for path /application/0/create-test-suite" in {
Post("/application/0/create-test-suite") ~> sut ~> check {
responseAs[Id] shouldEqual Id(0)
}
}

"not find any root path" in {
Get() ~> Route.seal(sut) ~> check {
status shouldEqual StatusCodes.NotFound
responseAs[String] shouldEqual "The requested resource could not be found."
}
}
}
}
45 changes: 0 additions & 45 deletions src/test/scala/de/retest/guistatemachine/WebServerSpec.scala

This file was deleted.

0 comments on commit decad8c

Please sign in to comment.