Skip to content

Unit Testing

Platform 6 has embedded the Spock testing framework to provide a feature rich test environment for P6 application developers.

https://spockframework.org/

Tests are written as Specifications using a clear and concise Groovy DSL, so making it a natural choice for integration with Platform 6 scripting.

There are a series of great tutorials to get you started here: https://www.softwaretestinghelp.com/spock-and-groovy/

Note

The Spock framework can also be used for Integration testing

Testing Conventions

  • A Spock test Specification is a Groovy Resource with the name extension of _test

New test specification

  • The name of the Specification class must match the resource name (without the _test extension)

Note

Creating a new test via the UI will automatically create a templated Specification class using the correct class name

  • When using p6sync all _test resources will be created under a test source root within the project filesystem:

    • src/test/groovy

Testing Guidelines

  • Specifications cannot test code embedded in the main script resource as this is not represented as a true Object and cannot be instantiated.
  • Specifications can only test Groovy resource classes
  • Specifications must be able to run independently without the services provided by a running P6 instance; these are UNIT tests Specifications
  • Specifications provide a simple, powerful DSL to mock other Objects. Mocking Objects is the key for writing Specifications that exercise code that interacts with other parts of Platform 6
  • Scripts created and updated via the scripts service UI are autoPackaged by default. i.e., Package declarations are not required. This is fine for small UI only developed projects. However, it is strongly recommended that larger project maintained via p6sync in an IDE should be manually packaged (all scripts requiring a package declaration)

Application Packaging

Test Specifications that become part of an application will be run each time the application is packaged

Any Specification that fails during the packaging process will cause the packaging to fail. This is an important step in enforcing a Test Driven Development principal within the Platform 6 development lifecycle.

Example Specification

import spock.lang.*

class TestSpec extends Specification {

    def "two plus two should equal four"() {
        given:
        int left = 2
        int right = 2

        when:
        int result = left + right

        then:
        result == 4
    }
}

Test Specifications can be run directly in the scripts service UI via the Run tests button.

Users of p6sync can take advantage of the gradle profile when creating a new application development project which will ensure Spock and Groovy are correctly configured allowing Specifications to be run within the IDE:

Test Specification in IDE

Example Script With Testing

As a reminder the main script cannot be tested, therefore create script resources for all processes which must be tested.

Take this main script, which uses a resource ExampleService to perform a process and puts the result in the pipeline.

import io.platform6.core.dsl.TypedP6Dsl

TypedP6Dsl p6 = new TypedP6Dsl(p6)

ExampleService example = new ExampleService(context: p6)

int result = example.exampleProcess(5)

p6.pipeline.put 'result', result.toString()

Here is exampleService, it contains the function exampleProcess which adds the value of an appconfig variable to the int provided, printing to logs before and after.

Note

Any calls to p6 DSL in script resources must be done by passing p6 to the resource upon creation, called context by convention.

import io.platform6.core.dsl.TypedP6Dsl

public class ExampleService {
    TypedP6Dsl context

    int exampleProcess(int providedValue){

        context.log.debug "providedValue is ${providedValue}"

        int processedValue = providedValue + context.appconfig.get('spockExampleCall')

        context.log.debug "processedValue is ${processedValue}"

        return processedValue

    }

}
Finally ExampleSpec, which tests for two cases, one where initial input is valid input and one where it is invalid, expecting an exception in the latter case.

import spock.lang.*

import io.platform6.core.dsl.TypedP6Dsl
import io.platform6.core.dsl.log.Log
import io.platform6.core.dsl.appconfig.AppConfig


class ExampleSpec extends Specification {

    @Shared
    TypedP6Dsl context


    def setupSpec() {
        context = Mock()
        Log log = Mock()
        context.log = log
    }

    def setup(){
        AppConfig appconfig = Mock()
        context.appconfig = appconfig
    }


    def "ExampleService works as expected with valid input"() {
        given:
        context.appconfig.get('spockExampleCall') >> 1
        ExampleService example = new ExampleService(context :context)

        when: "int provided"
        int result = example.exampleProcess(5)

        then:
        result == 6
    }

    def "ExampleService throws exception for invalid input"() {
        given:
        context.appconfig.get('spockExampleCall') >> 1
        ExampleService example = new ExampleService(context :context)

        when: "not int is provided"
        int result = example.exampleProcess("a")

        then:
        thrown groovy.lang.MissingMethodException
    }

}

To test ExampleService we must first mock the context and any calls upon it, such as the logs and appconfig.

  • Context is created as a shared variable so each method may use it to initalize ExampleService
  • Context and log ate then mocked in the setupSpec() which runs once before any tests are run.
  • The appconfig is mocked in setup() which runs once before each test, this is needed as we intend to set the return value of context.appconfig.get('spockExampleCall') but we don’t need the logs to actually do anything for the test.

Each test follows the given -> when -> then format.

  • In the given block each test sets the expected value of the appconfig call and initializes `ExampleService’.
  • In the when block exampleProcess is called and provided an input value.
  • In the then block the expected result is declared.

These tests are not extensive, they do not test for all types of problem inputs such as null, or the max possible integer, they also do not test for invalid values returned by the appconfig variable which could be configured incorrectly, or the call itself could fail.

Considering the possibilities for inputs and expected behaviours should shape the tests but also the script itself, you may want to catch specific exceptions and alert the user or perform the process anyway with a hardcoded safe value.