Integration Testing With ScalaTest and MongoDB

48
INTEGRATION TESTING WITH SCALATEST, MONGODB AND PLAY! EXPERIENCE FROM PLAY! PROJECT By / Michal Bigos @teliatko

description

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

Transcript of Integration Testing With ScalaTest and MongoDB

Page 1: Integration Testing With ScalaTest and MongoDB

INTEGRATION TESTINGWITH SCALATEST,

MONGODB AND PLAY!EXPERIENCE FROM PLAY! PROJECT

By /

Michal Bigos @teliatko

Page 2: Integration Testing With ScalaTest and MongoDB

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

Page 3: Integration Testing With ScalaTest and MongoDB

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

Page 4: Integration Testing With ScalaTest and MongoDB

INTEGRATION TESTING, WHY AND WHEN?PART ONE

Page 5: Integration Testing With ScalaTest and MongoDB

DEFINITIONWikipedia:

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

and tested as a group. ”

Page 6: Integration Testing With ScalaTest and MongoDB

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. ”

Page 7: Integration Testing With ScalaTest and MongoDB

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.

Page 8: Integration Testing With ScalaTest and MongoDB

UNIT TESTS 'VS' INTEGRATION TESTSUNIT TESTS TECHNIQUES:

MockingStubingxUnit frameworksFixtures in code

Page 9: Integration Testing With ScalaTest and MongoDB

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.

Page 10: Integration Testing With ScalaTest and MongoDB

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.

Page 11: Integration Testing With ScalaTest and MongoDB

UNIT TESTS 'VS' INTEGRATION TESTSKNOWN FRAMEWORKS:

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

Page 12: Integration Testing With ScalaTest and MongoDB

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.

Page 13: Integration Testing With ScalaTest and MongoDB

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")) { ...}

Page 14: Integration Testing With ScalaTest and MongoDB
Page 15: Integration Testing With ScalaTest and MongoDB

OUR CASEDEPENDENCIES BETWEEN COMPONENTS:

Page 16: Integration Testing With ScalaTest and MongoDB

OUR CASEGOALS:

Integration tests with real DAOs and DBWriting them like unit tests

Page 17: Integration Testing With ScalaTest and MongoDB

SCALATEST FOR INTEGRATION TESTING WITHMONGODB AND PLAY!

PART TWO

Page 18: Integration Testing With ScalaTest and MongoDB

TESTING STRATEGY

Responsibility - encapsulate domain logic

Unit test - testing the correctness of domain logic

Page 19: Integration Testing With ScalaTest and MongoDB

TESTING STRATEGY

Responsibility - read/save model

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

Page 20: Integration Testing With ScalaTest and MongoDB

TESTING STRATEGY

Responsibility - serialize/deserialize model to JSON

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

Page 21: Integration Testing With ScalaTest and MongoDB

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

Page 22: Integration Testing With ScalaTest and MongoDB

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) }}

Page 23: Integration Testing With ScalaTest and MongoDB

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

Page 24: Integration Testing With ScalaTest and MongoDB

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)}

Page 25: Integration Testing With ScalaTest and MongoDB

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")) { ...}

Page 26: Integration Testing With ScalaTest and MongoDB

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) ... } }}

Page 27: Integration Testing With ScalaTest and MongoDB

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)}

Page 28: Integration Testing With ScalaTest and MongoDB

*we love recursion in Scala isn't it?

Page 29: Integration Testing With ScalaTest and MongoDB

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 ))

}

Page 30: Integration Testing With ScalaTest and MongoDB

OUR SOLUTIONTypical test suite class

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

Page 31: Integration Testing With ScalaTest and MongoDB

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()}

Page 32: Integration Testing With ScalaTest and MongoDB

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)) }}

Page 33: Integration Testing With ScalaTest and MongoDB

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)) } }

Page 34: Integration Testing With ScalaTest and MongoDB

}

Page 35: Integration Testing With ScalaTest and MongoDB

CUSTOM DSL FOR INTEGRATION TESTING ANDSMALL EXTENSIONS TO CASBAH

PART THREEWORK IN PROGRESS

Page 36: Integration Testing With ScalaTest and MongoDB

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

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

Page 37: Integration Testing With ScalaTest and MongoDB

CUSTOM DSL FOR SEEDING THE DATAPrinciple

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

Page 38: Integration Testing With ScalaTest and MongoDB

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() { ... }

}

Page 39: Integration Testing With ScalaTest and MongoDB

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

Page 40: Integration Testing With ScalaTest and MongoDB

CUSTOM DSL FOR SEEDING THE DATAGoals

Pure functional solutionBetter fit with ScalaTestJUnit independent

Page 41: Integration Testing With ScalaTest and MongoDB

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")}

Page 42: Integration Testing With ScalaTest and MongoDB

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.

Page 43: Integration Testing With ScalaTest and MongoDB

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.

Page 44: Integration Testing With ScalaTest and MongoDB

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}

Page 45: Integration Testing With ScalaTest and MongoDB

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

Page 46: Integration Testing With ScalaTest and MongoDB

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

Page 47: Integration Testing With ScalaTest and MongoDB

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}

Page 48: Integration Testing With ScalaTest and MongoDB

THANKS FOR YOUR ATTENTION