Saturday, February 7, 2009

Unit testing: testing service in 3 different ways (part I)

Let's start with simple unit testing. If someone will be interested I could improve it with more complicated features. In part I we will use ExpandoMetaClass approach for unit testing.

For instance, we have simple Document domain class:

class Document {
String name
String description
Date creationTime
Date lastModifiedTime
Long revisionCount
User owner

static constraints = {
name(blank: false, maxSize: 64)
description(nullable: true, maxSize: 1024)
}

static belongsTo = [User]

String toString() {
"""Document [name: ${name},
description: ${description},
lastModifiedTime: ${lastModifiedTime}]"""
}

}
And let's create a simple Document service for handing save and retrieve operations:
class DocumentService {

def saveDocument(Document document)
throws InvalidDocumentException {
if (document.validate()) {
document.save()
} else {
throw new InvalidDocumentException(
document: document)
}
}

def retrieveDocumentByIdAndOwner(Long id, User owner)
throws DocumentNotFoundException {
Document document = Document.findByIdAndOwner(id, owner)
if (!document) {
throw new DocumentNotFoundException([id: id,
owner: owner].toMapString())
}

document
}

}
Lets create simple unit test for listed service using ExpandoMetaClass approach. As far as You remember during unit tests Grails does not inject any of the dynamic methods present during integration tests and at runtime. So we have to mock them in our tests.
class DocumentServiceTests extends GroovyTestCase {

def documentService

void setUp() {

// We have to initialize service class,
// there is no dependency injection.

documentService = new DocumentService()
}

void testSaveDocumentSuccess() {
def documentToSave = new Document(
name: "Document name",
description: "Document description",
lastModifiedTime: new Date(),
creationTime: new Date(),
revisionCount: 1L,
owner: new User())


// Dynamically add (mock) method validate to all
// instances of Document class and
// suppose it always return true.

Document.metaClass.static.validate = {
return true
}


// Dynamically add (mock) method save to all
// instances of Document class and
// suppose it always return true.

Document.metaClass.static.save = {
return true
}


// Saving will pass cause validate and
// save methods will return true.

documentService.saveDocument(documentToSave)
}

void testSaveDocumentFailure() {
def documentToSave = new Document(
name: "Document name",
description: "Document description",
lastModifiedTime: new Date(),
creationTime: new Date(),
revisionCount: 1L)


// Dynamically add (mock) method validate to all
// instances of Document class and
// suppose it always return false.

Document.metaClass.static.validate = {
return false
}


// Saving will fail cause validation will fail.

shouldFail(InvalidDocumentException) {
documentService.saveDocument(documentToSave)
}
}

void testRetrieveDocumentByIdAndOwnerSuccess() {

// Mock dynamic finder method to all
// instances of Document class and
// suppose it always return some document.

Document.metaClass.static.findByIdAndOwner = {
Long id, User owner ->
return new Document(id: id,
name: "Document name",
description: "Document description",
lastModifiedTime: new Date(),
creationTime: new Date(),
revisionCount: 1L,
owner: owner)
}

// Test the target method,
// it will return supposed document.

def foundDocument = documentService.retrieveDocumentByIdAndOwner(
1L, new User())

assertNotNull foundDocument
assert "Document name" == foundDocument.name
assert "Document description" == foundDocument.description
assert 1 == foundDocument.revisionCount
}

void testRetrieveDocumentByIdAndOwnerFailure() {

// Mock dynamic finder method to
// all instances of Document class and
// suppose it always return NULL.

Document.metaClass.static.findByIdAndOwner = {
Long id, User owner ->
return null
}


// Test the target method, it will fail cause
// no document instance will be found.

shouldFail(DocumentNotFoundException) {
documentService.retrieveDocumentByIdAndOwner(
1L, new User())
}
}

}
As You can see using ExpandoMetaClass is quiet easy. But be aware of using such approach cause some nuances are present:
  1. Not all dynamic methods can be mocked using metaClass property.
  2. Mocks using ExpandoMetaClass are created for whole tests (including integration tests). So if You mock some method for particular class in some unit test it will affect execution of mentioned method for particular class in other tests, so result can be unpredictable.

No comments:

Post a Comment