Date post: | 26-Aug-2014 |
Category: |
Self Improvement |
Upload: | michal-bigos |
View: | 3,275 times |
Download: | 1 times |
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}