INTEGRATION TESTINGWITH SCALATEST,MONGODB AND PLAY!EXPERIENCE FROM PLAY! PROJECTBy /Michal Bigos @teliatko
AGENDA1. Integration testing, why and when2. ScalaTest for integration testing with MongoDB and Play!3. Custom DSL for int...
CONTEXTFROM WHERE THIS ALL CAME FROM...Social network application with mobile clientsBuild on top of Play! 2Core API = RES...
INTEGRATION TESTING, WHY AND WHEN?PART ONE
DEFINITIONWikipedia:“ The phase in software testing in whichindividual software modules are combinedand tested as a group. ”
ANOTHER ONE :)Arquillian:“ Testing business components, in particular,can be very challenging. Often, a vanilla unittest i...
UNIT TESTS VS INTEGRATION TESTSUNIT TESTS PROPERTIES:Isolated - Checking one single concern in the system. Usuallybehavior...
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, but...
UNIT TESTS VS INTEGRATION TESTSVARIOUS INTEGRATION TESTS TYPES:Data-driven tests - Use real data and persistent store.In-c...
UNIT TESTS VS INTEGRATION TESTSKNOWN FRAMEWORKS:Data-driven tests - DBUnit, NoSQL Unit...In-container tests - Arquillian.....
WHY AND WHEN ?WHAT CANNOT BE WRITTEN/SIMULATED IN UNIT TESTInteraction with resources or sub-systems provided bycontainer....
OUR CASEARCHITECTURAL CONSTRAINTS LIMITING ISOLATION:Lack of DIController depends directly on DAOobject CheckIns extends C...
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 STRATEGYResponsibility - encapsulate domain logicUnit test - testing the correctness of domain logic
TESTING STRATEGYResponsibility - read/save modelIntegration test - testing the correctness of queries andmodifications, wi...
TESTING STRATEGYResponsibility - serialize/deserialize model to JSONIntegration test - testing the correctness of JSON out...
TESTING FRAMEWORKSSCALATESTStandalone xUnit frameworkCan be used within JUnit, TestNG...Pretty DSLs for writing test, espe...
TESTING FRAMEWORKSPLAY!S TESTING SUPPORTFake applicationReal HTTP serverit should "Test something dependent on Play! appli...
TESTING FRAMEWORKSDATA-DRIVEN TESTS FOR MONGODB- Mock implementation of the MongoDBprotocol and works purely in-memory.- M...
APPLICATION CODEConfiguration of MongoDB in application... another objecttrait MongoDBSetup {val MONGODB_URL = "mongoDB.ur...
APPLICATION CODEUse of MongoDBSetupin DAOsWe have to mock or provide real DB to test the DAOobject PubDao extends SalatDAO...
APPLICATION CODEControllers... youve seen this alreadyobject CheckIns extends Controller {...def generate(pubId: String) =...
OUR SOLUTIONEmbedding * to ScalaTestembedmongotrait EmbedMongoDB extends BeforeAndAfterAll { this: BeforeAndAfterAll with ...
*we love recursion in Scala isnt it?
OUR SOLUTIONCustom fake applicationTrait configures fake application instance for embeddedMongoDB instance. MongoDBSetupco...
OUR SOLUTIONTypical test suite classclass DataDrivenMongoDBTest extends FlatSpecwith ShouldMatcherswith MustMatcherswith E...
OUR SOLUTIONTest method which uses mongoDBinstance directlyit should "Save and read an Object to/from MongoDB" in {// Give...
OUR SOLUTIONTest method which uses DAO viafakeApplicationWithMongoit should "Save and read an Object to/from MongoDB which...
OUR SOLUTIONExample of the full test from controller down to modelclass FullWSTest extends FlatSpec with ShouldMatchers wi...
}
CUSTOM DSL FOR INTEGRATION TESTING ANDSMALL EXTENSIONS TO CASBAHPART THREEWORK IN PROGRESS
MORE DATACreating a simple data is easy, but what about collections...We need easy way to seed them from prepared source a...
CUSTOM DSL FOR SEEDING THE DATAPrincipleSeed the data before testUse them in test ... read, create or modifyCheck them aft...
CUSTOM DSL FOR SEEDING THE DATAInspiration - ,Based on JUnit rules or verbose codeNoSQL Unit DBUnitpublic class WhenANewBo...
This is Java. Example is taken from NoSQL Unit documentation.
CUSTOM DSL FOR SEEDING THE DATAGoalsPure functional solutionBetter fit with ScalaTestJUnit independent
CUSTOM DSL FOR SEEDING THE DATAResultit should "Load all Objcts from MongoDB" in {mongoDB seed ("users") fromFile ("./data...
CUSTOM DSL FOR SEEDING THE DATAAlready implementedSeeding, clean-up and clean-up after for functional andnon-funtional usa...
CUSTOM DSL FOR SEEDING THE DATAStill in pipelineChecking against dataset, similar to@ShouldMatchDataSet annotation of NoSQ...
TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAXWe dont like this*... I cannot read it, cant you?* and when possib...
TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAXWe like pretty code a lot ... like this:Casbah query DSL is our fa...
TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAXSo we enhanced it:def findBetweenDatesForPub(pubId: ObjectId, from...
TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERYPimp my library again and again...// Adds $eq operator instead of ->impli...
THANKS FOR YOUR ATTENTION
Integration Testing With ScalaTest and MongoDB
Upcoming SlideShare
Loading in …5
×

Integration Testing With ScalaTest and MongoDB

8,003 views

Published on

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

Integration Testing With ScalaTest and MongoDB

  1. 1. INTEGRATION TESTINGWITH SCALATEST,MONGODB AND PLAY!EXPERIENCE FROM PLAY! PROJECTBy /Michal Bigos @teliatko
  2. 2. AGENDA1. Integration testing, why and when2. ScalaTest for integration testing with MongoDB and Play!3. Custom DSL for integration testing and small extensions toCasbah
  3. 3. 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
  4. 4. INTEGRATION TESTING, WHY AND WHEN?PART ONE
  5. 5. DEFINITIONWikipedia:“ The phase in software testing in whichindividual software modules are combinedand tested as a group. ”
  6. 6. ANOTHER ONE :)Arquillian:“ Testing business components, in particular,can be very challenging. Often, a vanilla unittest isnt sufficient for validating such acomponents behavior. Why is that? Thereason is that components in an enterpriseapplication rarely perform operations whichare strictly self-contained. Instead, theyinteract with or provide services for thegreater system. ”
  7. 7. 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.
  8. 8. UNIT TESTS VS INTEGRATION TESTSUNIT TESTS TECHNIQUES:MockingStubingxUnit frameworksFixtures in code
  9. 9. UNIT TESTS VS INTEGRATION TESTSINTEGRATION TESTS PROPERTIES:Not isolated - Do not check the component or class itself, butrather integrated components together (sometimes wholeapplication).Slow - Depend on the tested component/sub-system.
  10. 10. 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.
  11. 11. UNIT TESTS VS INTEGRATION TESTSKNOWN FRAMEWORKS:Data-driven tests - DBUnit, NoSQL Unit...In-container tests - Arquillian...Performance tests - JMeter...Acceptance tests - Selenium, Cucumber...
  12. 12. WHY AND WHEN ?WHAT CANNOT BE WRITTEN/SIMULATED IN UNIT TESTInteraction 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.
  13. 13. OUR CASEARCHITECTURAL CONSTRAINTS LIMITING ISOLATION:Lack of DIController depends directly on DAOobject 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")) {...}
  14. 14. OUR CASEDEPENDENCIES BETWEEN COMPONENTS:
  15. 15. OUR CASEGOALS:Integration tests with real DAOs and DBWriting them like unit tests
  16. 16. SCALATEST FOR INTEGRATION TESTING WITHMONGODB AND PLAY!PART TWO
  17. 17. TESTING STRATEGYResponsibility - encapsulate domain logicUnit test - testing the correctness of domain logic
  18. 18. TESTING STRATEGYResponsibility - read/save modelIntegration test - testing the correctness of queries andmodifications, with real data and DB
  19. 19. TESTING STRATEGYResponsibility - serialize/deserialize model to JSONIntegration test - testing the correctness of JSON output,using the real DAOs
  20. 20. TESTING FRAMEWORKSSCALATESTStandalone xUnit frameworkCan be used within JUnit, TestNG...Pretty DSLs for writing test, especially FreeSpecPersonal preference over specs2Hooks for integration testing BeforeAndAfterandBeforeAndAfterAlltraits
  21. 21. TESTING FRAMEWORKSPLAY!S TESTING SUPPORTFake applicationReal HTTP serverit 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)}}
  22. 22. 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.jmockmongoNoSQL UnitEmbedMongo
  23. 23. APPLICATION CODEConfiguration of MongoDB in application... another objecttrait 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.configurationval 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)}
  24. 24. APPLICATION CODEUse of MongoDBSetupin DAOsWe have to mock or provide real DB to test the DAOobject PubDao extends SalatDAO[Pub, ObjectId](MongoDBSetup.mongoDB("pubs")) {...}
  25. 25. APPLICATION CODEControllers... youve seen this alreadyobject CheckIns extends Controller {...def generate(pubId: String) = Secured.withBasic { caller: User =>Action { implicit request =>val pubOpt = PubDao.findOneById(pubId)...}}}
  26. 26. OUR SOLUTIONEmbedding * to ScalaTestembedmongotrait 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.getDefaultInstancelazy val mongodExe: MongodExecutable = runtime.prepare(new MongodConfig(embedMongoDBVersion, embedConnectionPort, true))lazy val mongod: MongodProcess = mongodExe.start()override def beforeAll() {mongodsuper.beforeAll()}override def afterAll() {super.afterAll()mongod.stop(); mongodExe.stop()}lazy val mongoDB = MongoConnection(embedConnectionURL, embedConnectionPort)(embedDB)}
  27. 27. *we love recursion in Scala isnt it?
  28. 28. OUR SOLUTIONCustom fake applicationTrait configures fake application instance for embeddedMongoDB instance. MongoDBSetupconsumes this values.trait FakeApplicationForMongoDB extends MongoDBSetup { this: EmbedMongoDB =>lazy val fakeApplicationWithMongo = FakeApplication(additionalConfiguration = Map(MONGODB_PORT -> embedConnectionPort.toString,MONGODB_URL -> embedConnectionURL,MONGODB_DB -> embedDB))}
  29. 29. OUR SOLUTIONTypical test suite classclass DataDrivenMongoDBTest extends FlatSpecwith ShouldMatcherswith MustMatcherswith EmbedMongoDBwith FakeApplicationForMongoDB {...}
  30. 30. OUR SOLUTIONTest method which uses mongoDBinstance directlyit should "Save and read an Object to/from MongoDB" in {// Givenval users = mongoDB("users") // this is from EmbedMongoDB trait// Whenval user = User(username = username, password = password)users += grater[User].asDBObject(user)// Thenusers.count should equal (1L)val query = MongoDBObject("username" -> username)users.findOne(query).map(grater[User].asObject(_)) must equal (Some(user))// Clean-upusers.dropCollection()}
  31. 31. OUR SOLUTIONTest method which uses DAO viafakeApplicationWithMongoit should "Save and read an Object to/from MongoDB which is used in application" in {running(fakeApplicationWithMongo) {// Givenval user = User(username = username, password = password)// WhenUserDao.save(user)// ThenUserDao.findAll().find(_ == user) must equal (Some(user))}}
  32. 32. OUR SOLUTIONExample of the full test from controller down to modelclass 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.toStringval 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))}}
  33. 33. }
  34. 34. CUSTOM DSL FOR INTEGRATION TESTING ANDSMALL EXTENSIONS TO CASBAHPART THREEWORK IN PROGRESS
  35. 35. MORE DATACreating a simple data is easy, but what about collections...We need easy way to seed them from prepared source andcheck them afterwards.
  36. 36. CUSTOM DSL FOR SEEDING THE DATAPrincipleSeed the data before testUse them in test ... read, create or modifyCheck them after test (optional)
  37. 37. CUSTOM DSL FOR SEEDING THE DATAInspiration - ,Based on JUnit rules or verbose codeNoSQL Unit DBUnitpublic class WhenANewBookIsCreated {@ClassRulepublic static ManagedMongoDb managedMongoDb = newManagedMongoDbRule().mongodPath("/opt/mongo").build();@Rulepublic 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() {...}}
  38. 38. This is Java. Example is taken from NoSQL Unit documentation.
  39. 39. CUSTOM DSL FOR SEEDING THE DATAGoalsPure functional solutionBetter fit with ScalaTestJUnit independent
  40. 40. CUSTOM DSL FOR SEEDING THE DATAResultit should "Load all Objcts from MongoDB" in {mongoDB seed ("users") fromFile ("./database/data/users.json") andseed ("pubs") fromFile ("./database/data/pubs.json")cleanUpAfter {running(fakeApplicationWithMongo) {val users = UserDao.findAll()users.size must equal (10)}}// Probably will be deprecated in next versionsmongoDB seed ("users") fromFile ("./database/data/users.json") now()running(fakeApplicationWithMongo) {val users = UserDao.findAll()users.size must equal (10)}mongoDB cleanUp ("users")}
  41. 41. CUSTOM DSL FOR SEEDING THE DATAAlready implementedSeeding, clean-up and clean-up after for functional andnon-funtional usage.JSON fileformat similar to NoSQL Unit - difference, percollection basis.
  42. 42. CUSTOM DSL FOR SEEDING THE DATAStill in pipelineChecking 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.
  43. 43. TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAXWe dont like this*... I cannot read it, cant you?* and when possible we dont write thisdef 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}
  44. 44. TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAXWe like pretty code a lot ... like this:Casbah query DSL is our favorite ... even when it is notperfectdef findBetweenDatesForPub(pubId: ObjectId, from: DateTime, to: DateTime) : List[CheckIn] = {find {("pubId" -> pubId) ++("created" $gte from $lt to)} sort {("created" -> -1)}}.toList.headOption
  45. 45. TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERY SYNTAXSo 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
  46. 46. TOPPINGSMALL ADDITIONS TO CASBAH FOR BETTER QUERYPimp my library again and again...// Adds $eq operator instead of ->implicit def queryOperatorAdditions(field: String) = new {protected val _field = field} with EqualsOptrait EqualsOp {protected def _field: Stringdef $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}
  47. 47. THANKS FOR YOUR ATTENTION

×