Groovy

So If You’re Using Tag Libraries for Your View Models You Have To Test Them, Right?

In a previous post I introduced thinking about (visual) components and used a Task Browser as an example of an user interface “component”.
 
 
 
 
 
 
 
 
 
 
grails-task-overview-main-screen-wireframe

I explained that using

  • View Model(s) e.g. plain-old Groovy objects (POGOs) holding the related data e.g. a class TaskBrowser
  • Tag Libraries (LayoutTagLib) and tags (def taskBrowser) to render the associated HTML (views/layouts/components/_taskBrowser.gsp) to the page

allows for more maintainable and testable code.

Let’s put our money where our mouth is and see how one could test the used tag library.

The Parts

So these are the (simplified) parts in the equation.

Task – The domain class

class Task {
  enum Type { PERSONAL, WORK }

  String title
  Type type
}

TaskBrowser – Just a POGO with the data

class TaskBrowser {

  List tasks = []

  /**
   * Month to start with.
   *
   * @return number between 1 and 12
   */
  int getStartMonth() {
    def nowDate = new Date()
    nowDate[Calendar.MONTH] + 1
  }
}

HomeController – Creating the Task Browser in an index action.

class HomeController {
  def taskService

  def index() {
    [taskBrowser: new TaskBrowser(tasks: taskService.retrieveTasks())]
  }
}

home/index.gsp – The GSP for the index action

<!doctype html>
<html>
 <head>
 <meta name="layout" content="main" />
 <title>Tasks</title>
 </head>

 <body>
 <g:taskBrowser taskBrowser="${taskBrowser}"/>
 </body>
</html>

views/layouts/components/_taskBrowser.gsp – The Task Browser HTML

<div class="row month-${startMonth}" id="task-browser">
<div class="six columns">
  <g:if test="${tasks}">
  <%-- 500 lines more... --%>

LayoutTagLib – Finally, the tag library

/**
 * Renders the task browser.
 * 
 * @attr taskBrowser REQUIRED a task browser instance
 * @attr filter Optionally a {@link Task.Type} to show only those tasks
 */
def taskBrowser = { attrs ->
  if (!attrs.taskBrowser) {
    throwTagError("Tag [taskBrowser] is missing " +
                    "required attribute [taskBrowser]")
  }

  TaskBrowser browser = attrs.taskBrowser

  // filter tasks by type
  def tasks = browser.tasks
  if (attrs.filter && attrs.filter instanceof Task.Type) {
    tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
  } 

  out << render(template: '/layouts/components/taskBrowser', 
    model: [tasks : tasks,
      months : browser.months,
      startMonth : browser.startMonth
  ])
}

Unit Test

If you look at the tag library code there’s a few things interesting enough to cover with an unit test.

Normally whenever you use a Grails command to create a controller, service or tag library, also an associated unit test is created. If not, you can always create one later on with

grails create-unit-test LayoutTagLib

In any case, we start with a LayoutTagLibSpec which is initially pretty empty.

import grails.test.mixin.TestFor
import spock.lang.Specification

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 def setup() {
 }

 def cleanup() {
 }

 void "test something"() {
 expect:"fix me"
 true == false
 }
}

The @TestFor annotation is part of the Grails framework. It not only indicates the class-under-test (aka the actual unit we’re supposed to be testing here), but also gives us a concrete instance of that class.

More on that later.

Now we can implement our first test, called…

Task browser tag should show all tasks by default

Although the skeleton testmethod "test something" starts with “test…”, I try to omit that part. We’re a creating tests obviously and repeating “test xxx” up front has no additional value but taking up space.

If we were in the unit test of the TaskBrowser (e.g. TaskBrowserSpec) I would skip the name of the class-under-test from the testmethod e.g. “ task browser tag should show…”. Since we’re in a more generic LayoutTagLib I would like to know which tag – of many more to come ofcourse – we’re talking about, so I do start with “task browser tag…”

I usually start by placing my Given/When/Then Spock labels in the test method. This helps me structure my own head in thinking about

  1. What are the prequisites? (Given)
  2. What’s the actual code to invoke? (When)
  3. What’s there to assert or verify? (Then)

Here’s what I have now:

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {

   given:

   when:

   then:
 }
}

The actual invocation of the tag goes under When. Since the @TestFor annotation points to a tag library class, we’re given an implicit tagLib variable to work which references a clean LayoutTagLib instance each time.

Don’t do this in a unit test, because it’s the preTestFor way:

def layoutTagLib = new LayoutTagLib()
// or <span class="pl-k">def</span> layoutTagLib <span class="pl-k">=</span> applicationContext<span class="pl-k">.</span>getBean(Layout<span class="pl-k">TagLib</span>)
layoutTagLib.taskBrowser(...)

but use the tagLib, Luke.

tagLib.taskBrowser(...)

So we have this:

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {

   given:

   when:
   tagLib.taskBrowser()

   then:
 }
}

I know that the happy-path flow for this test needs a TaskBrowser instance. There should be al least one task to verify the tag should show it by default. So let’s add them:

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {
 given:
 def task = new Task(title: "My task")
 def browser = new TaskBrowser(tasks: [task])

 when:
 tagLib.taskBrowser(taskBrowser: browser)

 then:
 true
 }
}

Hey, why is true there in the Then block? It’s because we need to have a Then block after When, to be able to execute this test once at this moment. Normally we would write this probably with Expect as

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {

   given:

   expect:

 }
}

but I’m too lazy to update it to Expect and change it back later to When/Then&#55357;&#56841; We need the Then later on anyway. What Grails does in a unit test for a tag library is actually rendering the template /layouts/components/_taskBrowser.gsp using the supplied model.

Remember the code for the LayoutTagLib?

/**
 * Renders the task browser.
 * 
 * @attr taskBrowser REQUIRED a task browser instance
 * @attr filter Optionally a {@link Task.Type} to show only those tasks
 */
def taskBrowser = { attrs ->
  if (!attrs.taskBrowser) {
    throwTagError("Tag [taskBrowser] is missing " +
                    "required attribute [taskBrowser]")
  }

  TaskBrowser browser = attrs.taskBrowser

  // filter tasks by type
  def tasks = browser.tasks
  if (attrs.filter && attrs.filter instanceof Task.Type) {
    tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
  } 

  out << render(template: '/layouts/components/taskBrowser', 
    model: [tasks : tasks,
      months : browser.months,
      startMonth : browser.startMonth
  ])
}

If above (simplistic) test succeeds, the _taskBrowser.gsp — and its logic — hasn’t failed with an exception. You can make a typo in your template(s) and see the evaluation fail. Covering just the evaluation of a GSP might be worth a test, but we haven’t checked anything of the contents.

How do we know if the correct template is referenced? How do we know if the correct model is actually passed along?

The Inevitable Truth

If you look at the Testing chapter of the Grails documentation, you’ll see a simplistic example of testing the response of a SimpleTagLib

class SimpleTagLib {
  static namespace = 's'

  def hello = { attrs, body ->
    out << "Hello ${attrs.name ?: 'World'}"
  }
@TestFor(SimpleTagLib)
class SimpleTagLibSpec extends Specification {

  void "test tag calls"() {
    expect:
    tagLib.hello().toString() == 'Hello World'

Our tag isn’t as simple as returning “Hello World” – our tag renders 500 lines of Sophisticated Task Browser HTML to the output buffer. Verifying this the simple way isn’t that simple.

There are a few approaches here.

#1 – Checking for parts

Often contains is used in pieces of code which simply need to be checked for the presence of an item. We could try to verify the existence of our single task “My task” somewhere in the output like this:

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {
   given:
   def task = new Task(title: "My task")
   def browser = new TaskBrowser(tasks: [task])

   when:
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()

   then:
   result.contains "My task"
 }
}

We have verified successfully that “My task” is visible somewhere in 500 lines of output.

(Psst, quickly beef up the test before we continue – make sure we always have at least a multiple amount of test items to check logic which deals with a collection, instead of a single item)

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {
   given:
   def task1 = new Task(title: "My 1st task")
   def task2 = new Task(title: "My 2nd task")
   def browser = new TaskBrowser(tasks: [task1, task2])

   when:
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()

   then:
   result.contains "My 1st task"
   result.contains "My 2nd task"
 }
}

The disadvantage is that we’ve coupled our test for the tag lib logic (showing or filtering tasks) with the rendering of the HTML (presence of the title of a task)

To alleviate this a bit we should be…

#2 – Controlling what parts are rendered

Just as with Controller tests we can use a feature of the ControllerUnitTestMixin to mock the view used for rendering.

Use the implicit getViews() or getGroovyPages() — which return a Map for us to manipulate. Overwrite the real template with custom content of our own, in which we control what & how the model is rendered.

First make sure we actually overwrite the correct template path, by letting the test fail. The taskBrowser tag says render(template: '/layouts/components/taskBrowser'... so we have to put alternative contents under key '/layouts/components/_taskBrowser.gsp' — there’s a discrepancy in the format of writing the template path.

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {
   given:
   def task1 = new Task(title: "My 1st task")
   def task2 = new Task(title: "My 2nd task")
   def browser = new TaskBrowser(tasks: [task1, task2])

   when:
   views['/layouts/components/_taskBrowser.gsp'] = 'bogus'
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()

   then:
   result.contains "My 1st task"
   result.contains "My 2nd task"
 }
}

This fails correctly…

Condition not satisfied:

result.contains "My 1st task"
|      |
bogus  false

…so we know we have the correct key.

Now choose proper contents.

Just printing the collection of tasks should give us all we need to verify that our 2 tasks have been passed to the model to our custom template now.

Condition not satisfied:

result.contains "My 1st task"
|      |
|      false
[sample.Task : (unsaved), sample.Task : (unsaved)]

Dang!

We can (and should) not rely on the String-representation of a Task class, our model.

Don’t go adding a toString() method! We could adjust the test to detect the presence of the correct items by a unique property e.g. the title.

@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {

 void "task browser tag should show all tasks by default"() {
   given:
   def task1 = new Task(title: "My 1st task")
   def task2 = new Task(title: "My 2nd task")
   def browser = new TaskBrowser(tasks: [task1, task2])

   when:
   views['/layouts/components/_taskBrowser.gsp'] = '${tasks.title}'
   // make sure only titles are rendered e.g. [My 1st task, My 2nd task]
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()

   then:
   result.contains "My 1st task"
   result.contains "My 2nd task"
 }
}

This succeeds!

And is ugly. We’re still relying on coercing the result from the rendering (StreamCharBuffer) to a String which need to be compared as a whole with something we expect or checked for parts. For testing some small HTML snippets this is fine though.

There are a few tips and tricks I’ll share in a future post as the model becomes too complex to test through this mechanism. For now we’ll be flying with this mechanism.

Rinse and repeat

I often take the first, simple test as a basis for further tests. In a 2nd test we need to verify if the task browser can actually filter for once on task types, such as Personal or Work.

Introduced a filter by passing along a filter attribute to the tag. The resulting test could look like the following. We’re checking that only one task which has that type is rendered, and not the other.

void "task browser tag should show only personal tasks"() {
 given:
 def task1 = new Task(title: "My 1st task", type: Type.PERSONAL)
 def task2 = new Task(title: "My 2nd task", type: Type.WORK)
 def browser = new TaskBrowser(tasks: [task1, task2])

 and: 
 def filterType = Type.PERSONAL

 when:
 views['/layouts/components/_taskBrowser.gsp'] = '${tasks.title}'
 def result = tagLib.taskBrowser(taskBrowser: browser, 
 filter: filterType).toString()

 then:
 result.contains "My 1st task"
 !result.contains("My 2nd task")
}

(Normally, I would have supplied all kinds of labels to the given:, when: etc. blocks, but I’ll leave that as an exercise for the reader.)

If I weren’t too tired of spending a weekend with Romans and gladiators I would have written some more variations and a concluding part, but I hope just writing above down here on the Ted Vinke Blog will be of some help to someone.

Happy testing!

Ted Vinke

Ted is a Java software engineer with a passion for Web development and JVM languages and works for First8, a Java Web development company in the Netherlands.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button