Testing
Write unit tests
Test utilities are available to make it easier to test your script. Start by adding the cereal-test-utils dependency to your build.gradle file if it's not there already:
There is a TestScriptRunner available which accepts an instance of your script:
To run your script using the TestScriptRunner, call the run method. This method accepts the script's
configuration and a ComponentProviderFactory, which is used by the runner to create a ComponentProvider.
This way, both the configuration and the component provider can be mocked to test the behavior of your
app under different circumstances. A full example:
class TestSampleScript {
@Test
fun testSuccess() = runBlocking {
// Initialize script and the test script runner.
val script = MyScript()
val scriptRunner = TestScriptRunner(script)
// Mock the LicenseChecker
mockkConstructor(LicenseChecker::class)
coEvery { anyConstructed<LicenseChecker>().checkAccess() } returns LicenseState.Licensed
// Mock the configuration values
val configuration = mockk<SampleConfiguration>(relaxed = true) {
every { myConfigurationProperty() } returns "Some random string"
}
val componentProviderFactory = TestComponentProviderFactory()
try {
// Run the script with a 10s timeout. This is needed because most scripts don't end within a reasonable time.
// If your script is expected to end automatically please remove the surrounding try catch block.
withTimeout(10000) { scriptRunner.run(configuration, componentProviderFactory) }
} catch(e: Exception) {
// Ignore timeouts because they're expected.
}
}
}
Mocking a secret
A Secret configuration item is mocked like any other — return a real Secret wrapping
whatever plaintext your test needs. There is no test double to configure:
val configuration = mockk<SampleConfiguration>(relaxed = true) {
every { apiKey() } returns Secret("test-api-key")
}
Your script reaches that plaintext through reveal(), so assertions on what the script sent to a third party work
unchanged. Two things to keep in mind when writing assertions:
Secretnever renders its value.assertEquals("test-api-key", secret.toString())fails —toString()returns the mask. Comparesecret.reveal(), or compare againstSecret("test-api-key")directly, sinceSecrethas value-based equality.- A failing assertion shows the mask, not the value. Test output and diffs show
***for aSecret, which is the type working as intended. If a comparison fails and you need to see the actual value, compare the revealed strings so the failure message is readable.
For a per-task secret, mock the item the same way — the runner supplies the value your mock returns to each task.
Supplying complex list rows
A complex list has no default values, so a test has to supply the rows itself. Override the configuration method and return an anonymous implementation of your record interface per row:
val configuration = mockk<PurchaseConfiguration>(relaxed = true) {
every { targets() } returns listOf(
object : Target {
override fun sku() = "SKU-1"
override fun qty() = 2
override fun size() = Size.MEDIUM
override fun notify() = true
},
)
}
Return an empty list to test what your script does with no rows — but note that Cereal itself never delivers an empty
list for a non-nullable List<T>, so prefer null (with a nullable declaration) to represent "not configured".
Configuring the test components
TestComponentProviderFactory builds a ComponentProvider whose components are in-memory stand-ins for the real
ones. Four properties let you script what those stand-ins return:
| Property | Feeds | Purpose |
|---|---|---|
childScriptConfigurations |
ScriptLauncherComponent.start |
Maps each child script class to the configuration the runner should start it with. |
showUrlResults |
UserInteractionComponent.showUrl |
Queue of WebResourceRequest values returned by successive showUrl calls. |
showHtmlResults |
UserInteractionComponent.showHtml |
Queue of WebResourceRequest values returned by successive showHtml calls. |
requestInputResults |
UserInteractionComponent.requestInput |
Queue of strings returned by successive requestInput calls. |
Each list is consumed in order — the first showUrl call gets the first entry, the second call the second, and so on.
If your script makes more calls than you supplied results for, the component throws with a message naming the index
it wanted, so a failing test tells you exactly which entry to add.
val componentProviderFactory = TestComponentProviderFactory().apply {
// Return a "logged in" redirect from the first showUrl call
showUrlResults = listOf(
WebResourceRequest(
method = "GET",
requestHeaders = emptyMap(),
url = "https://example.com/success",
postData = null,
),
)
// Answer the script's 2FA prompt
requestInputResults = listOf("123456")
// Required before a child script can be started
childScriptConfigurations = mapOf(
MyChildScript::class.java to mockk<MyChildConfiguration>(relaxed = true),
)
}
Two behaviours to be aware of when writing assertions:
shouldFinishmust accept the first result. The test user-interaction component calls yourshouldFinishpredicate once per queued result and throws if it returnsfalse. Real runs poll every resource request until the predicate matches, so queue the request that does satisfy it.- Preferences are shared between tests.
TestPreferenceComponentkeeps its values in a single store shared across instances, so state written by one test is visible to the next. Use distinct keys per test, or clear the keys your script writes, if ordering-independent tests matter to you.
Child scripts started through the launcher run on a real background coroutine, and start throws if the class is
missing the @ChildScript annotation or has no entry in childScriptConfigurations.
Test in Cereal application
Using unit tests, you're probably able to test a large portion of your script. Nonetheless, it's recommended to test the behavior of your script in a real environment, such as the Cereal application, to also verify if your script is correctly obfuscated by ProGuard.
To do this, create a new script in the Cereal Developer Console and upload your script as a draft. Afterwards, open the Cereal desktop application and make sure to log in with an account that you used to upload the script, or any account that belongs to the same team under which the script is uploaded. Go to Settings and enable "Show development scripts" to also download scripts in development. The script should now be visible in the list of scripts. If an update is released, it will automatically be updated in the Cereal application after a restart or a manual sync.