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:
Designate a folder for all test related files
Write your unit test code
Setup your unit test to run with Gradle
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:
1# Enable Gradle’s 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 Engine’s 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
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}
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}
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}
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.
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.
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
$ 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