• Document Up to Date

Unit Testing CrafterCMS Groovy Code

For larger sites with complex services implemented in Groovy, it is very helpful to have a way to include unit tests in a way that can be easily integrated with CI/CD systems.

This section details how to create unit tests for CrafterCMS Groovy code with Gradle.

For more information on the classes of the variables that can be mocked for unit testing, see Groovy API

Steps for Creating Groovy Unit Test

To create a unit test:

  1. Designate a folder for all test related files

  2. Write your unit test code

  3. Setup your unit test to run with Gradle

  4. Execute your unit test

Write your unit test code

There are no restrictions or requirements for the unit test code, developers can choose any testing framework supported by the build tool. Examples: spring-test, junit, testng, spock

Remember when writing unit test code, developers will be responsible for:

  • Choosing & configuring the testing framework

  • Making sure all required dependencies are included (for example external jars)

  • Mocking all Crafter Engine classes used by the classes under testing

Setup your unit test to run with Gradle

To use Gradle the only requirement is to add a build.gradle in the root folder of the site repository and execute the test task. Example:

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/build.gradle
 1# Enable Gradles Groovy plugin
 2plugins {
 3  id 'groovy'
 4}
 5
 6sourceSets {
 7  # Add the site Groovy classes that will be tested
 8  main {
 9    groovy {
10      srcDir 'scripts/classes'
11    }
12  }
13
14  # Add the Groovy classes & resources to perform the tests
15  test {
16    groovy {
17      srcDir 'scripts/test/classes'
18    }
19    resources {
20      srcDir 'scripts/test/resources'
21    }
22  }
23}
24
25# Enable the testing framework of choice
26test {
27  useJUnit()
28}
29
30repositories {
31  mavenCentral()
32}
33
34# Include the required dependencies
35dependencies {
36  # This dependency is required for two reasons:
37  # 1. Make Engines classes available for compilation & testing
38  # 2. Include the Groovy dependencies required by Gradle
39  implementation 'org.craftercms:crafter-engine:3.1.13:classes'
40
41  # Include the chosen testing dependencies
42  testImplementation 'junit:junit:4.13.2'
43  testImplementation 'org.mockito:mockito-core:3.9.0'
44}

Execute your unit test

Given the previous example the tests can be executed using a single command:

gradle test

Example

Let’s take a look at an example of a groovy unit test in a site created using the empty blueprint with a custom groovy script, MyService

Unit Testing Groovy - Sample Service

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/api/MyService.groovy
 1package org.company.site.api
 2
 3/*
 4  Custom service that will be invoked from other controllers like scheduled jobs, page or REST scripts
 5 */
 6
 7interface MyService {
 8
 9    def saveFormSubmission(def title, def author, def message)
10
11    def getPostViews(def postId)
12
13    def getPostsSummary(def date)
14
15}

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/api/MySearchService.groovy
 1package org.company.site.api
 2
 3/*
 4  Service that wraps elasticsearch to hide queries & result mapping from other services
 5 */
 6
 7interface MySearchService {
 8
 9    def getPostsForDate(def date)
10
11}

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/api/ExternalApi.groovy
 1package org.company.site.api
 2
 3/*
 4  External service to store & retrieve data, in real life this could be using an external REST API or DB
 5 */
 6
 7interface ExternalApi {
 8
 9    def saveFormSubmission(def formId, def title, def author, def message)
10
11    def getPostViews(def postId)
12
13}

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/classes/org/company/site/impl/MyServiceImpl.groovy
 1package org.company.site.impl
 2
 3import org.company.site.api.MySearchService
 4import org.craftercms.core.service.CachingOptions
 5import org.craftercms.core.util.cache.CacheTemplate
 6import org.company.site.api.ExternalApi
 7import org.company.site.api.MyService
 8import org.craftercms.engine.service.context.SiteContext
 9
10/*
11  Implementation of the custom service
12 */
13
14class MyServiceImpl implements MyService {
15
16    protected ExternalApi externalApi
17
18    protected CacheTemplate cacheTemplate
19
20    protected MySearchService searchService
21
22    protected def formId
23
24    protected def refreshFrequency
25
26    def saveFormSubmission(def title, def author, def message) {
27        return externalApi.saveFormSubmission(formId, title, author, message)
28    }
29
30    def getPostViews(def postId) {
31        def context = SiteContext.current.context
32        def options = new CachingOptions()
33        options.refreshFrequency = refreshFrequency
34        return cacheTemplate.getObject(context, options, { externalApi.getPostViews(postId) }, "post-views-", postId)
35    }
36
37    def getPostsSummary(def date) {
38        def posts = searchService.getPostsForDate(date)
39        return posts.collect { post ->
40            [
41                    id: post.id,
42                    views: getPostViews(post.id)
43            ]
44        }
45    }
46
47}

Let’s begin creating our unit test for MyService

Designate folder for all test related files

The first thing we need to do is to designate a folder for all test related files. We’ll designate the /scripts/test folder to be used for all test related files.

Write your unit test code

Next, we’ll write the unit test code.

Unit Testing Groovy - Sample Service

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/scripts/test/classes/org/company/impl/MyServiceImplTest.groovy
  1package org.company.site.impl
  2
  3import org.company.site.api.ExternalApi
  4import org.company.site.api.MySearchService
  5import org.craftercms.core.service.Context
  6import org.craftercms.core.util.cache.CacheTemplate
  7import org.craftercms.engine.service.context.SiteContext
  8import org.junit.Before
  9import org.junit.Test
 10import org.junit.runner.RunWith
 11import org.mockito.InjectMocks
 12import org.mockito.Mock
 13import org.mockito.Spy
 14import org.mockito.junit.MockitoJUnitRunner
 15
 16import static org.mockito.ArgumentMatchers.any
 17import static org.mockito.ArgumentMatchers.eq
 18import static org.mockito.Mockito.verify
 19import static org.mockito.Mockito.when
 20
 21/*
 22  Tests for the custom service
 23 */
 24
 25@RunWith(MockitoJUnitRunner)
 26class MyServiceImplTest {
 27
 28    static final def FORM_ID = "myFormId"
 29
 30    static final def POST_ID_1 = "myPostId1"
 31
 32    static final def POST_ID_2 = "myPostId2"
 33
 34    static final def POST_VIEWS_1 = 42
 35
 36    static final def POST_VIEWS_2 = 0
 37
 38    static final def REFRESH_FREQ = 5
 39
 40    @Spy
 41    SiteContext siteContext
 42
 43    @Mock
 44    Context context
 45
 46    @Mock
 47    CacheTemplate cacheTemplate
 48
 49    @Mock
 50    MySearchService searchService
 51
 52    @Mock
 53    ExternalApi externalApi
 54
 55    @InjectMocks
 56    MyServiceImpl myServiceImpl
 57
 58    @Before
 59    void setUp() {
 60        myServiceImpl.formId = FORM_ID
 61        myServiceImpl.refreshFrequency = REFRESH_FREQ
 62
 63        siteContext.context = context
 64        SiteContext.current = siteContext
 65
 66        when(externalApi.saveFormSubmission(eq(FORM_ID), any(), any(), any())).thenReturn(true)
 67        when(externalApi.getPostViews(eq(POST_ID_1))).thenReturn(POST_VIEWS_1)
 68        when(externalApi.getPostViews(eq(POST_ID_2))).thenReturn(POST_VIEWS_2)
 69
 70        when(cacheTemplate.getObject(eq(context), any(), any(), any())).then({ i -> i.getArgument(2).execute() })
 71
 72        when(searchService.getPostsForDate(any())).thenReturn([
 73                [ id: POST_ID_1 ],
 74                [ id: POST_ID_2]
 75        ])
 76    }
 77
 78    @Test
 79    void testFormSubmission() {
 80        def title = "My New Post"
 81        def author = "John Doe"
 82        def message = "Is this working?"
 83
 84        assert myServiceImpl.saveFormSubmission(title, author, message) == true
 85        verify(externalApi).saveFormSubmission(eq(FORM_ID), eq(title), eq(author), eq(message))
 86    }
 87
 88    @Test
 89    void testGetPostViews() {
 90        assert myServiceImpl.getPostViews(POST_ID_1) == 42
 91    }
 92
 93    @Test
 94    void testGetPostsSummary() {
 95        def result = myServiceImpl.getPostsSummary(new Date())
 96
 97        assert result.size() == 2
 98        assert result[0].views == POST_VIEWS_1
 99        assert result[1].views == POST_VIEWS_2
100    }
101
102}

Setup your unit test to run with Gradle

We’ll now setup our unit test to run with Gradle, by adding a build.gradle file in the root folder of the site repository and execute the test task.

CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/build.gradle
  1package org.company.site.impl
  2
  3import org.company.site.api.ExternalApi
  4import org.company.site.api.MySearchService
  5import org.craftercms.core.service.Context
  6import org.craftercms.core.util.cache.CacheTemplate
  7import org.craftercms.engine.service.context.SiteContext
  8import org.junit.Before
  9import org.junit.Test
 10import org.junit.runner.RunWith
 11import org.mockito.InjectMocks
 12import org.mockito.Mock
 13import org.mockito.Spy
 14import org.mockito.junit.MockitoJUnitRunner
 15
 16import static org.mockito.ArgumentMatchers.any
 17import static org.mockito.ArgumentMatchers.eq
 18import static org.mockito.Mockito.verify
 19import static org.mockito.Mockito.when
 20
 21/*
 22  Tests for the custom service
 23 */
 24
 25@RunWith(MockitoJUnitRunner)
 26class MyServiceImplTest {
 27
 28    static final def FORM_ID = "myFormId"
 29
 30    static final def POST_ID_1 = "myPostId1"
 31
 32    static final def POST_ID_2 = "myPostId2"
 33
 34    static final def POST_VIEWS_1 = 42
 35
 36    static final def POST_VIEWS_2 = 0
 37
 38    static final def REFRESH_FREQ = 5
 39
 40    @Spy
 41    SiteContext siteContext
 42
 43    @Mock
 44    Context context
 45
 46    @Mock
 47    CacheTemplate cacheTemplate
 48
 49    @Mock
 50    MySearchService searchService
 51
 52    @Mock
 53    ExternalApi externalApi
 54
 55    @InjectMocks
 56    MyServiceImpl myServiceImpl
 57
 58    @Before
 59    void setUp() {
 60        myServiceImpl.formId = FORM_ID
 61        myServiceImpl.refreshFrequency = REFRESH_FREQ
 62
 63        siteContext.context = context
 64        SiteContext.current = siteContext
 65
 66        when(externalApi.saveFormSubmission(eq(FORM_ID), any(), any(), any())).thenReturn(true)
 67        when(externalApi.getPostViews(eq(POST_ID_1))).thenReturn(POST_VIEWS_1)
 68        when(externalApi.getPostViews(eq(POST_ID_2))).thenReturn(POST_VIEWS_2)
 69
 70        when(cacheTemplate.getObject(eq(context), any(), any(), any())).then({ i -> i.getArgument(2).execute() })
 71
 72        when(searchService.getPostsForDate(any())).thenReturn([
 73                [ id: POST_ID_1 ],
 74                [ id: POST_ID_2]
 75        ])
 76    }
 77
 78    @Test
 79    void testFormSubmission() {
 80        def title = "My New Post"
 81        def author = "John Doe"
 82        def message = "Is this working?"
 83
 84        assert myServiceImpl.saveFormSubmission(title, author, message) == true
 85        verify(externalApi).saveFormSubmission(eq(FORM_ID), eq(title), eq(author), eq(message))
 86    }
 87
 88    @Test
 89    void testGetPostViews() {
 90        assert myServiceImpl.getPostViews(POST_ID_1) == 42
 91    }
 92
 93    @Test
 94    void testGetPostsSummary() {
 95        def result = myServiceImpl.getPostsSummary(new Date())
 96
 97        assert result.size() == 2
 98        assert result[0].views == POST_VIEWS_1
 99        assert result[1].views == POST_VIEWS_2
100    }
101
102}

Execute your unit test

Finally, we can run our unit test by running gradle test

Output when running unit test
$ gradle test

BUILD SUCCESSFUL in 4s
3 actionable tasks: 3 up-to-date

Let’s take a look at the result of our unit test which can be found here: CRAFTER_HOME/data/repos/sites/SITENAME/sandbox/build/reports/tests/test/index.html

Unit Testing Groovy - Unit test  build report