Integration Testing With ScalaTest and MongoDB

Post on 26-Aug-2014

3.274 views 1 download

Tags:

description

Our path to integration testing on Scala project using Play! framework, ScalaTest and MongoDB.

Transcript of Integration Testing With ScalaTest and MongoDB

INTEGRATION TESTINGWITH SCALATEST,

MONGODB AND PLAY!EXPERIENCE FROM PLAY! PROJECT

By /

Michal Bigos @teliatko

AGENDA1. Integration testing, why and when2. ScalaTest for integration testing with MongoDB and Play!3. Custom DSL for integration testing and small extensions to

Casbah

CONTEXTFROM WHERE THIS ALL CAME FROM...

Social network application with mobile clientsBuild on top of Play! 2Core API = REST servicesMongoDB used as main persistent storeHosted on HerokuCurrently in beta

INTEGRATION TESTING, WHY AND WHEN?PART ONE

DEFINITIONWikipedia:

“ The phase in software testing in whichindividual software modules are combined

and tested as a group. ”

ANOTHER ONE :)Arquillian:

“ Testing business components, in particular,can be very challenging. Often, a vanilla unit

test isn't sufficient for validating such acomponent's behavior. Why is that? The

reason is that components in an enterpriseapplication rarely perform operations which

are strictly self-contained. Instead, theyinteract with or provide services for the

greater system. ”

UNIT TESTS 'VS' INTEGRATION TESTSUNIT TESTS PROPERTIES:

Isolated - Checking one single concern in the system. Usuallybehavior of one class.

Repeateable - It can be rerun as meny times as you want.

Consistent - Every run gets the same results.

Fast - Because there are loooot of them.

UNIT TESTS 'VS' INTEGRATION TESTSUNIT TESTS TECHNIQUES:

MockingStubingxUnit frameworksFixtures in code

UNIT TESTS 'VS' INTEGRATION TESTSINTEGRATION TESTS PROPERTIES:

Not isolated - Do not check the component or class itself, butrather integrated components together (sometimes whole

application).

Slow - Depend on the tested component/sub-system.

UNIT TESTS 'VS' INTEGRATION TESTSVARIOUS INTEGRATION TESTS TYPES:

Data-driven tests - Use real data and persistent store.In-container tests - Simulates real container deployment,e.g. JEE one.Performance tests - Simulate traffic growth.Acceptance tests - Simulate use cases from user point ofview.

UNIT TESTS 'VS' INTEGRATION TESTSKNOWN FRAMEWORKS:

Data-driven tests - DBUnit, NoSQL Unit...In-container tests - Arquillian...Performance tests - JMeter...Acceptance tests - Selenium, Cucumber...

WHY AND WHEN ?WHAT CANNOT BE WRITTEN/SIMULATED IN UNIT TEST

Interaction with resources or sub-systems provided bycontainer.Interaction with external systems.Usage of declarative services applied to component atruntime.Testing whole scenarions in one test.Architectural constraints limits isolation.

OUR CASEARCHITECTURAL CONSTRAINTS LIMITING ISOLATION:

Lack of DI

Controller depends directly on DAO

object CheckIns extends Controller { ...

def generate(pubId: String) = Secured.withBasic { caller: User => Action { implicit request => val pubOpt = PubDao.findOneById(pubId) ... } }}

object PubDao extends SalatDAO[Pub, ObjectId](MongoDBSetup.mongoDB("pubs")) { ...}

OUR CASEDEPENDENCIES BETWEEN COMPONENTS:

OUR CASEGOALS:

Integration tests with real DAOs and DBWriting them like unit tests

SCALATEST FOR INTEGRATION TESTING WITHMONGODB AND PLAY!

PART TWO

TESTING STRATEGY

Responsibility - encapsulate domain logic

Unit test - testing the correctness of domain logic

TESTING STRATEGY

Responsibility - read/save model

Integration test - testing the correctness of queries andmodifications, with real data and DB

TESTING STRATEGY

Responsibility - serialize/deserialize model to JSON

Integration test - testing the correctness of JSON output,using the real DAOs

TESTING FRAMEWORKSSCALATEST

Standalone xUnit frameworkCan be used within JUnit, TestNG...Pretty DSLs for writing test, especially FreeSpecPersonal preference over specs2Hooks for integration testing BeforeAndAfter andBeforeAndAfterAll traits

TESTING FRAMEWORKSPLAY!'S TESTING SUPPORT

Fake application

Real HTTP server

it should "Test something dependent on Play! application" in { running(FakeApplication()) { // Do something which depends on Play! application }}

"run in a server" in { running(TestServer(3333)) { await(WS.url("http://localhost:3333").get).status must equalTo(OK) }}

TESTING FRAMEWORKSDATA-DRIVEN TESTS FOR MONGODB

- Mock implementation of the MongoDBprotocol and works purely in-memory.

- More general library for testing with variousNoSQL stores. It can provide mocked or real MongoDBinstance. Relies on JUnit rules.

- Platform independent way of running localMongoDB instances.

jmockmongo

NoSQL Unit

EmbedMongo

APPLICATION CODEConfiguration of MongoDB in application

... another object

trait MongoDBSetup { val MONGODB_URL = "mongoDB.url" val MONGODB_PORT = "mongoDB.port" val MONGODB_DB = "mongoDB.db"}

object MongoDBSetup extends MongoDBSetup {

private[this] val conf = current.configuration

val url = conf.getString(MONGODB_URL).getOrElse(...) val port = conf.getInt(MONGODB_PORT).getOrElse(...) val db = conf.getString(MONGODB_DB).getOrElse(...)

val mongoDB = MongoConnection(url, port)(db)}

APPLICATION CODEUse of MongoDBSetup in DAOs

We have to mock or provide real DB to test the DAO

object PubDao extends SalatDAO[Pub, ObjectId](MongoDBSetup.mongoDB("pubs")) { ...}

APPLICATION CODEControllers

... you've seen this already

object CheckIns extends Controller { ...

def generate(pubId: String) = Secured.withBasic { caller: User => Action { implicit request => val pubOpt = PubDao.findOneById(pubId) ... } }}

OUR SOLUTIONEmbedding * to ScalaTestembedmongo

trait EmbedMongoDB extends BeforeAndAfterAll { this: BeforeAndAfterAll with Suite => def embedConnectionURL: String = { "localhost" } def embedConnectionPort: Int = { 12345 } def embedMongoDBVersion: Version = { Version.V2_2_1 } def embedDB: String = { "test" }

lazy val runtime: MongodStarter = MongodStarter.getDefaultInstance lazy val mongodExe: MongodExecutable = runtime.prepare(new MongodConfig(embedMongoDBVersion, embedConnectionPort, true)) lazy val mongod: MongodProcess = mongodExe.start()

override def beforeAll() { mongod super.beforeAll() }

override def afterAll() { super.afterAll() mongod.stop(); mongodExe.stop() }

lazy val mongoDB = MongoConnection(embedConnectionURL, embedConnectionPort)(embedDB)}

*we love recursion in Scala isn't it?

OUR SOLUTIONCustom fake application

Trait configures fake application instance for embeddedMongoDB instance. MongoDBSetup consumes this values.

trait FakeApplicationForMongoDB extends MongoDBSetup { this: EmbedMongoDB =>

lazy val fakeApplicationWithMongo = FakeApplication(additionalConfiguration = Map( MONGODB_PORT -> embedConnectionPort.toString, MONGODB_URL -> embedConnectionURL, MONGODB_DB -> embedDB ))

}

OUR SOLUTIONTypical test suite class

class DataDrivenMongoDBTest extends FlatSpec with ShouldMatchers with MustMatchers with EmbedMongoDB with FakeApplicationForMongoDB { ...}

OUR SOLUTIONTest method which uses mongoDB instance directly

it should "Save and read an Object to/from MongoDB" in { // Given val users = mongoDB("users") // this is from EmbedMongoDB trait

// When val user = User(username = username, password = password) users += grater[User].asDBObject(user)

// Then users.count should equal (1L) val query = MongoDBObject("username" -> username) users.findOne(query).map(grater[User].asObject(_)) must equal (Some(user))

// Clean-up users.dropCollection()}

OUR SOLUTIONTest method which uses DAO viafakeApplicationWithMongo

it should "Save and read an Object to/from MongoDB which is used in application" in { running(fakeApplicationWithMongo) { // Given val user = User(username = username, password = password)

// When UserDao.save(user)

// Then UserDao.findAll().find(_ == user) must equal (Some(user)) }}

OUR SOLUTIONExample of the full test from controller down to model

class FullWSTest extends FlatSpec with ShouldMatchers with MustMatchers with EmbedMongoDB with FakeApplicationForMongoDB {

val username = "test" val password = "secret" val userJson = """{"id":"%s","firstName":"","lastName":"","age":-1,"gender":-1,"state":"notFriends","photoUrl":""}"""

"Detail method" should "return correct Json for User" in { running(TestServer(3333, fakeApplicationWithMongo)) { val users = mongoDB("users") val user = User(username = username, password = md5(username + password)) users += grater[User].asDBObject(user)

val userId = user.id.toString val response = await(WS.url("http://localhost:3333/api/user/" + userId) .withAuth(username, password, AuthScheme.BASIC) .get())

response.status must equal (OK) response.header("Content-Type") must be (Some("application/json; charset=utf-8")) response.body must include (userJson.format(userId)) } }

}

CUSTOM DSL FOR INTEGRATION TESTING ANDSMALL EXTENSIONS TO CASBAH

PART THREEWORK IN PROGRESS

MORE DATACreating a simple data is easy, but what about collections...

We need easy way to seed them from prepared source andcheck them afterwards.

CUSTOM DSL FOR SEEDING THE DATAPrinciple

Seed the data before testUse them in test ... read, create or modifyCheck them after test (optional)

CUSTOM DSL FOR SEEDING THE DATAInspiration - ,

Based on JUnit rules or verbose code

NoSQL Unit DBUnit

public class WhenANewBookIsCreated {

@ClassRule public static ManagedMongoDb managedMongoDb = newManagedMongoDbRule().mongodPath("/opt/mongo").build();

@Rule public MongoDbRule remoteMongoDbRule = new MongoDbRule(mongoDb().databaseName("test").build());

@Test @UsingDataSet(locations="initialData.json", loadStrategy=LoadStrategyEnum.CLEAN_INSERT) @ShouldMatchDataSet(location="expectedData.json") public void book_should_be_inserted_into_repository() { ... }

}

This is Java. Example is taken from NoSQL Unit documentation.

CUSTOM DSL FOR SEEDING THE DATAGoals

Pure functional solutionBetter fit with ScalaTestJUnit independent

CUSTOM DSL FOR SEEDING THE DATAResult

it should "Load all Objcts from MongoDB" in { mongoDB seed ("users") fromFile ("./database/data/users.json") and seed ("pubs") fromFile ("./database/data/pubs.json") cleanUpAfter { running(fakeApplicationWithMongo) { val users = UserDao.findAll() users.size must equal (10) } }

// Probably will be deprecated in next versions mongoDB seed ("users") fromFile ("./database/data/users.json") now() running(fakeApplicationWithMongo) { val users = UserDao.findAll() users.size must equal (10) } mongoDB cleanUp ("users")}

CUSTOM DSL FOR SEEDING THE DATAAlready implemented

Seeding, clean-up and clean-up after for functional andnon-funtional usage.JSON fileformat similar to NoSQL Unit - difference, percollection basis.

CUSTOM DSL FOR SEEDING THE DATAStill in pipeline

Checking against dataset, similar to@ShouldMatchDataSet annotation of NoSQL Unit.JS file format of mongoexport. Our biggest problem hereare Dates (proprietary format).JS file format with full JavaScript functionality of mongocommand. To be able to run commands like:db.pubs.ensureIndex({loc : "2d"})NoSQL Unit JSON file format with multiple collections andseeding more collections in once.

TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAX

We don't like this*

... I cannot read it, can't you?* and when possible we don't write this

def findCheckInsBetweenDatesInPub( pubId: String, dateFrom: LocalDateTime, dateTo: LocalDateTime) : List[CheckIn] = { val query = MongoDBObject("pubId" -> new ObjectId(pubId), "created" -> MongoDBObject("$gte" -> dateFrom, "$lt" -> dateTo)) collection.find(query).map(grater[CheckIn].asObject(_)).toList.headOption}

TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAX

We like pretty code a lot ... like this:

Casbah query DSL is our favorite ... even when it is notperfect

def findBetweenDatesForPub(pubId: ObjectId, from: DateTime, to: DateTime) : List[CheckIn] = { find { ("pubId" -> pubId) ++ ("created" $gte from $lt to) } sort { ("created" -> -1) }}.toList.headOption

TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAX

So we enhanced it:

def findBetweenDatesForPub(pubId: ObjectId, from: DateTime, to: DateTime) : List[CheckIn] = { find { ("pubId" $eq pubId) ++ ("created" $gte from $lt to) } sort { "created" $eq -1 } }.headOption

TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY

Pimp my library again and again...

// Adds $eq operator instead of ->implicit def queryOperatorAdditions(field: String) = new { protected val _field = field} with EqualsOp

trait EqualsOp { protected def _field: String def $eq[T](target: T) = MongoDBObject(_field -> target)}

// Adds Scala collection headOption operation to SalatCursorimplicit def cursorAdditions[T <: AnyRef](cursor: SalatMongoCursor[T]) = new { protected val _cursor = cursor} with CursorOperations[T]

trait CursorOperations[T <: AnyRef] { protected def _cursor: SalatMongoCursor[T] def headOption : Option[T] = if (_cursor.hasNext) Some(_cursor.next()) else None}

THANKS FOR YOUR ATTENTION