with one click
test-quality
Spock/Groovy testing standards and best practices for CPS project.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Spock/Groovy testing standards and best practices for CPS project.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | test-quality |
| description | Spock/Groovy testing standards and best practices for CPS project. |
This document describes important aspects of our Spock/Groovy test framework, conventions, and best practices observed in the CPS-NCMP codebase.
spock.lang.SpecificationListAppender)Mono, Flux)Spec suffixDataJobServiceImplSpec, TrustLevelManagerSpec, NetworkCmProxyFacadeSpecdef 'Description of what is being tested'()then: blocks with descriptions to express expectations insteaddef 'Registration with invalid cm handle name.'()def 'Registration of invalid cm handle throws exception.'()def 'Read data job request.'()def 'DMI Registration: Create, Update, Delete & Upgrade operations are processed in the right order'()def 'Initial cm handle registration with a cm handle that is not trusted'()mock (e.g., mockInventoryPersistence, mockDmiSubJobRequestHandler)objectUnderTest (not custom names like parameterMapper or class-specific names)result for the outcome of method callsmy or some for values that don't affect the test (e.g., 'my user', 'some scope')Tests use labeled blocks for clarity and readability:
def 'Test description'() {
given: 'context setup description'
// Setup code
and: 'additional context'
// More setup
when: 'action being tested'
// Execute the method under test
then: 'expected outcome'
// Assertions and verifications
and: 'additional expectations'
// More assertions
}
Pattern with constructor injection and mocks:
class DataJobServiceImplSpec extends Specification {
def mockWriteRequestExaminer = Mock(WriteRequestExaminer)
def mockDmiSubJobRequestHandler = Mock(DmiSubJobRequestHandler)
def objectUnderTest = new DataJobServiceImpl(
mockDmiSubJobRequestHandler,
mockWriteRequestExaminer,
mockJsonObjectMapper
)
}
Alternative with Spy for partial mocking:
def objectUnderTest = Spy(new CmHandleRegistrationService(
mockPropertyHandler,
mockInventoryPersistence,
...
))
Stubbing return values:
mockInventoryPersistence.getYangModelCmHandle('ch-1') >> new YangModelCmHandle(id: 'ch-1')
Verifying method calls:
1 * mockInventoryEventProducer.sendAvcEvent('ch-1', 'trustLevel', 'NONE', 'COMPLETE')
0 * mockInventoryEventProducer.sendAvcEvent(*_) // No calls expected
Argument matching:
1 * mockWriteRequestExaminer.splitDmiWriteOperationsFromRequest('my-job-id', dataJobWriteRequest)
mockAlternateIdChecker.getIdsOfCmHandlesWithRejectedAlternateId(*_) >> [] // Any arguments
Use where block for data tables:
def 'Determining cloud event using ce_type header for a #scenario.'() {
given: 'headers contain a header for key: #key'
headers.lastHeader(key) >> header
expect: 'the check for cloud events returns #expectedResult'
assert objectUnderTest.isCloudEvent(headers) == expectedResult
where: 'the following headers (keys) are defined'
scenario | key || expectedResult
'cloud event' | 'ce_type' || true
'non-cloud event' | 'other' || false
}
Conventions:
#variableName in test name to include parameter values in test output| to separate input columns, || before expected output columnswhere block are accessible throughout the testdef setup() {
setupLogger(Level.DEBUG)
mockAlternateIdChecker.getIdsOfCmHandlesWithRejectedAlternateId(*_) >> []
}
def cleanup() {
((Logger) LoggerFactory.getLogger(DataJobServiceImpl.class)).detachAndStopAllAppenders()
hazelcastInstance.shutdown()
}
Groovy assert (preferred):
assert result.statusCode == HttpStatus.OK
assert trustLevelPerCmHandleId.get('ch-1') == TrustLevel.COMPLETE
Spock with block for complex assertions:
with(logger.list.find { it.level == Level.DEBUG }) {
assert it.formattedMessage.contains("Initiating WRITE operation")
}
JUnit-style assertions (less common):
assertEquals(expected, actual)
assertTrue(condition)
def logger = Spy(ListAppender<ILoggingEvent>)
def setup() {
def setupLogger = ((Logger) LoggerFactory.getLogger(DataJobServiceImpl.class))
setupLogger.setLevel(Level.DEBUG)
setupLogger.addAppender(logger)
logger.start()
}
def 'Test with logging verification'() {
when: 'method is called'
objectUnderTest.someMethod()
then: 'correct log message is generated'
def loggingEvent = logger.list[0]
assert loggingEvent.level == Level.INFO
assert loggingEvent.formattedMessage.contains('Expected message')
}
def 'Get resource data from DMI (delegation).'() {
given: 'a cm resource address'
def cmResourceAddress = new CmResourceAddress('ncmp-datastore:operational', 'ch-1', 'resource-id')
and: 'get resource data returns a Mono'
mockHandler.executeRequest(cmResourceAddress, 'options', NO_TOPIC, false, 'auth') >>
Mono.just('dmi response')
when: 'get resource data is called'
def response = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, ...).block()
then: 'response is correct'
assert response == 'dmi response'
}
then: blocks with descriptions insteaddef 'Registration with invalid cm handle name.'() with then: 'a validation exception is thrown'def 'Registration of invalid cm handle throws exception.'()def 'Initial cm handle registration with a cm handle that is not trusted'()def 'test1'()and blocks to break up complex setupsgiven: 'a invalid cm handle name'
cmHandle.id = 'invalid,name'
when: 'permission is checked for unauthorized user'
objectUnderTest.checkPermission(someCmh, 'my user', 'some scope')
then: 'exception message contains the user id'
thrown(PermissionException).message.contains('my user')
setup() when applicableassert over JUnit assertionsnew CmHandle(id: 'invalid,name')NO_TOPIC = null)my or some0 * to ensure methods are NOT called when expected*_ (any arguments) sparingly; prefer specific argument matching#paramNamethen: blocks insteadobjectUnderTest and result, not custom namesmy or some prefixes for arbitrary values to distinguish from important onesgiven:, when:, then: descriptions synchronized with codewhere blocks: Don't duplicate similar tests; parameterize insteadcps-ncmp-service/src/test/groovy/