In part three of our Snippets tutorial series we are going to introduce how Grails handles Many-to-Many relationships. We will do this by introducing another domain class that will allow us to “tag” our snippets. Finally I will look at another plugin used in the testing area - Code Coverage and the benefits it’s brings to a project.
Tagging Our Snippets
Organising thing into folders is so web 1.0, tagging is so web 2.0, tagging’s the future man, THE FUTURE! Well maybe not but in an argument of tagging vs folder organisation - tagging is bound to win. In fact adding a single tag to something is the same as putting it a folder! But having the power use multiple tags produces a much clearer view (usually unless you’re a tag maniac) of the information you are trying to convey.
With that in mind I am going to create the ability to tag out snippets. I am however going to leave the category property in. I know they sort of clash but I have good reason for doing this which will be made clearer in the future. Tagging will be handled by our Tag domain class which will form one side of our Many-to-Many relationship - a tag can have many snippets and a snippet can have many tags. Lets create out Tag class.
> grails create-domain-class Tag
Populate the generated Tag.groovy like so…
class Tag {
String name
static constraints = {
name(unique:true, blank:false, nullable:false)
}
}
Now we need to set up the Many-To-Many relationship with the Snippets domain class. This involves adding the hasMany and belongsTo properties to the classes.
class Tag {
static belongsTo = Snippet
static hasMany = [snippets:Snippet]
...
}
and
class Snippet {
static hasMany = [tags:Tag]
...
}
This is our Many-to-Many relationship created - at least at the back end. When we get into into creating/generating our views we will need to manually define how this relationship is managed as Grails currently doesn’t generate anything for Many-to-Many relationship. I know imagine having to actually do some work yourself! Nightmare! Thankfully it’s very easy - and for another time.
Constraining Tagging
I made a statement earlier about “tag maniac”s. If you look at a site that features tagging abilities (last.fm or del.icio.us are perfect examples) you will find some people who feel they need to tag everything throughly. On last.fm you might get a band called, for example, “My Band” and you’ll likely find tags such as
band, music, songs, singer, bassist, guitar, drums, band_name_contains_letter_m, band_name_contains_letter_y, band_name_contains_letter_b, bands_with_space_in_name etc etc
It’s not real but I have seen similar craziness in action (I used to be an obsesive tagger myself). We wouldn’t want this happening here so to reel the user in a bit we want to also add a restriction on the amount of tags a person can use - lets say 10. It’ll also be good to require the user to enter at least one tag so we can add the size constraint to the Snippet domain class.
class Snippet {
...
static constraints = {
...
tags(size:1..10)
}
}
Because we added the hasMany property with a property name of tags Grails will generate a property in the class called tags which will be a list of Tag objects (it also adds a heap of dynamic methods for working with the list e.g. addToTags()).
NB. I thought this would work but it was causing my tests to fail. The max size aspect of the size constraint worked but for some reason the minimum size didn’t work. It tired a few things but no luck (also discovered that apparently size doesn’t work when the nullable constraint is also used on the same property). I resolved this by using my own custom validation.
class Snippet {
...
static constraints = {
...
tags(validator: {val, obj ->
val != null && (1..10).contains(val.size())
})
}
}
Fixing Our Tests
The addition of the size constraint on our tag list has now broken our tests we created in the last post - it has also introduced some new properties which need to be tested. At the very least we should update our current test set so that they pass. If we run the current tests again you will see that out Snippet test testValidSnippetCreation will fail because we are required to specify at least one tag let update this test to work,
void testValidSnippetCreation() {
def snippet = new Snippet(
title:'Success Test',
category:'dummy',
code:"Simple Test",
tags:[new Tag(name:"test")]
)
assert snippet.validate() == true
}
Run the tests again and everything should pass. Nice!
Code Coverage
Testing is all good and well but it is incredibly easy to just not test everything. Manually having to ensure you have checked every possible path isn’t always easy. We have added a Tag class and haven’t written any tests for it. Now the class itself is actually small and I already know in advance that it’s functionality is already tested inadvertently within out Snippet tests so lets do something now - lets add another function, something that the Snippet tests do not use.
class Tag {
...
boolean compareNames(Tag otherTag){
name.equals(otherTag.name)
}
}
Granted this is very contrived but it happens in reality all the time, trust me. Now this code is valid but how would we know? It doesn’t get used in any of the current tests. To help avoid these situations there are tools available and Grails makes such a tool available as a plugin. The code-coverage plugin uses a popular tool called Cobertura which will generate reports and highlight areas of you system that have went untested Lets install it,
> grails install-plugin code-coverage
Now this is installed we can run an extended test cycle that includes a code coverage check
> grails test-app-cobertura
This, just like test-app, will run the tests and generate reports (now in the cobertura directory under the test results). If you look at these reports you should see that the Tag class hasn’t been fully tested and delving deeper you should also should see it is because the compareNames function isn’t tested.
You may also notice that the RefCode plugin files fail a bit of code coverage. I am not sure how to make the code-coverage explicity ignore these files - if I find out i’ll update this post.
So lets beef up our Snippet tests and add some tests for the Tag class.
class SnippetTests extends GroovyTestCase {
...
void testInvalidSnippetCreationNotEnoughTags001() {
def snippet = new Snippet(
title:'Success Test',
category:'dummy',
code:"Simple Test"
)
assert snippet.validate() == false
}
void testInvalidSnippetCreationTooManyTags001() {
def snippet = new Snippet(
title:'Success Test',
category:'dummy',
code:"Simple Test",
tags:[
new Tag(name:"test01"),
new Tag(name:"test02"),
new Tag(name:"test03"),
new Tag(name:"test04"),
new Tag(name:"test05"),
new Tag(name:"test06"),
new Tag(name:"test07"),
new Tag(name:"test08"),
new Tag(name:"test09"),
new Tag(name:"test10"),
new Tag(name:"test11")
]
)
assert snippet.validate() == false
}
void testInvalidSnippetCreationNotEnoughTags002() {
def snippet = new Snippet(
title:'Success Test',
category:'dummy',
code:"Simple Test",
tags:[]
)
assert snippet.validate() == false
}
void testInvalidSnippetCreationTooManyTags002() {
def snippet = new Snippet(
title:'Success Test',
category:'dummy',
code:"Simple Test",
tags:[new Tag(name:"test01")]
)
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test02"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test03"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test04"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test05"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test06"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test07"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test08"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test09"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test10"))
assert snippet.validate() == true
snippet.addToTags(new Tag(name:"test11"))
assert snippet.validate() == false
}
}
class TagTests extends GroovyTestCase {
void setUp() {
new Tag(name:"Java").save()
}
void testInvalidTagCreationNoName() {
def tag = new Tag()
assert tag.validate() == false
}
void testInvalidTagCreationBreakUniqueName() {
def tag2 = new Tag(name:"Java")
assert tag2.validate() == false
}
void testInvalidTagCreationBlankName() {
def tag = new Tag(name:"")
assert tag.validate() == false
}
void testValidTagCreation() {
def tag = new Tag(name:"NewTag")
assert tag.validate() == true
}
void testCompareNames001(){
assert new Tag(name:"NewTag").compareNames(new Tag(name:"NewTag")) == true
}
void testCompareNames002(){
assert new Tag(name:"NewTag").compareNames(new Tag(name:"OldTag")) == false
}
}
You are more than welcome to write more tests but this is sufficient for this project.
Summary
That’s part 3 is wrapped up. Next step will be to make the solution work in a multi-user environment so we will introduce the User object - that will wrap up the core of our domain classes and we can move onto the front end and the controllers.
Post a Comment