Here you can find out how to create integration tests for RESTful service on the example of application shown in the article “Building REST service with Scala“. To create the tests, I am going to use spray-testkit DSL, as it provides a simple way to test route logic for services built with spray-routing.
Also, to keep everything up to date, I’ve updated the RESTful service built in the article to use Spray version 1.2.1. You can find the updated code that works with newer versions of Spray, Akka and Lift JSON here.
To run the service and be sure that everything works correctly, specify the right mysql parameters in the src/main/resources/application.conf file and then create appropriate MySQL database.
To run the service and be sure that everything works correctly, specify the right mysql parameters in the src/main/resources/application.conf file and then create appropriate MySQL database.
So, let’s get to implementing the tests:
Step 1. Configure dependencies
At first, configure build.sbt to use spray-testkit with specs2 by adding the following dependencies:
libraryDependencies ++= Seq(
"io.spray" % "spray-testkit" % "1.2.1" % "test",
"org.specs2" %% "specs2" % "2.3.13" % "test"
)
Code language: JavaScript (javascript)
spray-testkit also supports ScalaTest, so it can be used instead of specs2.
Step 2. Configure database
To use a separate database for the testing, create the application.conf config file in the src/test/scala/resources/ and specify the db.name
property:
<code>db {
// the database name
name = "restTest"
}</code>
Code language: JavaScript (javascript)
You can also configure any other property that is required for your test environment (see the example of the base config file here).
Step 3. Initialize service for the testing
Move implicits from the RestService trait to the CustomerConversions object to use them later in the tests:
object CustomerConversions {
implicit val liftJsonFormats = new Formats {
val dateFormat = new DateFormat {
val sdf = new SimpleDateFormat("yyyy-MM-dd")
def parse(s: String): Option[Date] = try {
Some(sdf.parse(s))
} catch {
case e: Exception => None
}
def format(d: Date): String = sdf.format(d)
}
}
implicit def HttpEntityToCustomer(httpEntity: HttpEntity) =
Serialization.read[Customer](httpEntity.asString(HttpCharsets.`UTF-8`))
}
Code language: JavaScript (javascript)
I’ve placed the implicits in Customer.scala, but you can use a separate file.
Create the CustomerServiceTestBase trait in the test folder, it will be the base trait for our tests.
CustomerServiceTestHelper should extend: Specification
(to use specs2 matchers), Specs2RouteTest
(to use spray routing DSL) and HttpService
to connect the DSL to the test ActorSystem by setting actorRefFactory
. Also, it should mixin the specs2 Before
trait to run the code which cleans the database before test execution.
Then add the actorRefFactory
to connect the DSL to the test ActorSystem, implement the service initialization to get the spray route instance for test requests, and create the following methods:
cleanDB
– to clean customers table before test executionimplicit HttpEntityToListOfCustomers
– to convert JSON response from service to List[Customer]implicit HttpEntityToErrors
– to convert JSON response with error description to Map[String, String]
import scala.language.existentials
import org.specs2.mutable.{Before, Specification}
import spray.testkit.Specs2RouteTest
import spray.routing.HttpService
import akka.actor.ActorRefFactory
import spray.http.{HttpCharsets, HttpEntity}
import net.liftweb.json.Serialization
import com.sysgears.example.domain.{Customers, Customer}
import com.sysgears.example.domain.CustomerConversions._
import com.sysgears.example.config.Configuration
import scala.slick.session.Database
import scala.slick.driver.MySQLDriver.simple.Database.threadLocalSession
import scala.slick.driver.MySQLDriver.simple._
import slick.jdbc.meta.MTable
trait CustomerServiceTestBase extends Specification
with Specs2RouteTest with HttpService
with Configuration with Before {
// makes test execution sequential and prevents conflicts that may occur when the data is
// changed simultaneously in the database
args(sequential = true)
val customerLink = "/customer"
// connects the DSL to the test ActorSystem
implicit def actorRefFactory = system
val spec = this
val customerService = new RestService {
override implicit def actorRefFactory: ActorRefFactory = spec.actorRefFactory
}.rest
/**
* Cleans the database before test execution.
*/
def cleanDB() = {
// inits database instance
val db = Database.forURL(url = s"jdbc:mysql://$dbHost:$dbPort/$dbName",
user = dbUser, password = dbPassword, driver = "com.mysql.jdbc.Driver")
// drops tables if exist
db.withSession {
if (MTable.getTables("customers").list().nonEmpty) {
Customers.ddl.drop
Customers.ddl.create
}
}
}
// converts responses from the service
implicit def HttpEntityToListOfCustomers(httpEntity: HttpEntity) =
Serialization.read[List[Customer]](httpEntity.asString(HttpCharsets.`UTF-8`))
implicit def HttpEntityToErrors(httpEntity: HttpEntity) =
Serialization.read[Map[String, String]](httpEntity.asString(HttpCharsets.`UTF-8`))
}
Code language: JavaScript (javascript)
Step 4. Create test data
Obviously, we need test data to run the tests, so let’s create the CustomerTestData object with the list of two consumers and the date formatter for responses:
import java.text.SimpleDateFormat
import com.sysgears.example.domain.Customer
object CustomerTestData {
val dateFormat = new SimpleDateFormat("yyyy-MM-dd")
// the test data
val birthday0 = Some(dateFormat.parse("1991-01-14"))
val firstName0 = "Andrey"
val lastName0 = "Litvinenko"
val birthday1 = Some(dateFormat.parse("1987-01-14"))
val firstName1 = "Corwin"
val lastName1 = "Holmes"
val customersIds = List(1, 2)
val customers = List(
Customer(Some(customersIds(0)), firstName0, lastName0, birthday0),
Customer(Some(customersIds(1)), firstName1, lastName1, birthday1))
val nonExistentCustomerId = customers.size + 1
}
Code language: JavaScript (javascript)
If you need more data for tests, you might want to create a function to generate random data.
Step 5. Create tests
Now we are finally ready for writing the tests.
All the tests follow a certain template:
"service" should {
"do something" in { // action description
request ~> service ~> check { // request to service
// response checks:
status should be equalTo ???
entity should be equalTo ???
}
}
}
Code language: JavaScript (javascript)
We are going to clean the DB before running the tests, and then test the main service functionality by sending CRUD requests and checking the responses.
Here is the shortened version of tests, you can see the full version in repository:
import spray.http.HttpEntity
import net.liftweb.json.Serialization
import spray.http.HttpMethods._
import spray.http.StatusCodes._
import com.sysgears.example.domain.Customer
// imports the object with test data:
import com.sysgears.example.rest.CustomerTestData._
import com.sysgears.example.domain.CustomerConversions._
import spray.http.HttpRequest
import scala.Some
class CustomerServiceSpec extends CustomerServiceTestBase {
def before = cleanDB()
"Customer service" should {
"post customers" in {
HttpRequest(POST, customerLink, entity = HttpEntity(
Serialization.write(customers(0)))) ~> customerService ~> check {
response.status should be equalTo Created
response.entity should not be equalTo(None)
val respCustomer = responseAs[Customer]
respCustomer.id.get must be greaterThan 0
respCustomer must be equalTo customers(0)
}
HttpRequest(POST, customerLink, entity = HttpEntity(
Serialization.write(customers(1)))) ~> customerService ~> check {
response.status should be equalTo Created
response.entity should not be equalTo(None)
val respCustomer = responseAs[Customer]
respCustomer.id.get must be greaterThan 0
respCustomer must be equalTo customers(1)
}
}
"return list of posted customers" in {
Get(customerLink) ~> customerService ~> check {
response.status should be equalTo OK
response.entity should not be equalTo(None)
val respCustomers = responseAs[List[Customer]]
respCustomers.size should be equalTo 2
respCustomers(0) must be equalTo customers(0)
respCustomers(1) must be equalTo customers(1)
}
}
"search for customers" in {
"search by firstName" in {
Get(s"$customerLink?firstName=$firstName0") ~> customerService ~> check {
response.status should be equalTo OK
response.entity should not be equalTo(None)
val respCustomer = responseAs[Customer]
respCustomer must be equalTo customers(0)
}
}
}
"return customer by id" in {
Get(s"$customerLink/${customersIds(0)}") ~> customerService ~> check {
response.status should be equalTo OK
response.entity should not be equalTo(None)
responseAs[Customer] must be equalTo customers(0)
}
}
"update customer data" in {
HttpRequest(PUT, s"$customerLink/${customersIds(0)}", entity = HttpEntity(
Serialization.write(customers(1)))) ~> customerService ~> check {
response.status should be equalTo OK
response.entity should not be equalTo(None)
responseAs[Customer]
.copy(id = Some(customersIds(1))) must be equalTo customers(1)
}
Get(s"$customerLink/${customersIds(0)}") ~> customerService ~> check {
response.status should be equalTo OK
response.entity should not be equalTo(None)
responseAs[Customer]
.copy(id = Some(customersIds(1))) must be equalTo customers(1)
}
}
"delete created customers by id" in {
customersIds.map {
id =>
Delete(s"$customerLink/$id") ~> customerService ~> check {
response.status should be equalTo OK
response.entity should not be equalTo(None)
}
}.find(!_.isSuccess) === None
}
"return 404 and error message when we try to delete a non-existent customer" in {
customersIds.map {
id =>
Delete(s"$customerLink/$id") ~> customerService ~> check {
response.status should be equalTo NotFound
response.entity should not be equalTo(None)
responseAs[Map[String, String]].get("error") ===
Some(s"Customer with id=$id does not exist")
}
}.find(!_.isSuccess) === None
}
}
}
Code language: JavaScript (javascript)
See also:
- spray-testkit documentation
- specs2 documentation
Hope you found this helpful.