Skip to main content

Testing API

Integration testing

info

GenesisJunit is only available from version 8 of the Genesis Server Framework (GSF). If you are testing against previous versions of the framework, see the page on legacy testing

Genesis provides a Junit 5 extension that helps developers write integration tests. The extension will create a Genesis microservice for the duration of each test case. The GenesisJunit functionality is made available via the genesis-testsupport package. This page will deal with the GenesisJunit extension in general.

Testing specific services

In addition to general test support; GenesisJunit also provides support for specific services. For more information, have a look at the links below.

A simple example

Let's have a look at a simple test. We have a TradeProcessor class that takes a trade Id, processes the trade and updates the status. We want to make sure our trade is marked as processed, once complete.

@ExtendWith(GenesisJunit::class) // 1
@CsvData("my-trades.csv") // 2
class TradeProcessorTest {

@Inject
lateinit var db: SyncEntityDb // 3

@Inject
lateinit var tradeProcessor: TradeProcessor // 4

@Test
fun myTest() {
tradeProcessor.processTrade("TRADE_1") // 5

val dbTrade = db.get(Trade.byId("TRADE_1")) // 6

assert(dbTrade.status == TradeStatus.PROCESSED) // 7
}
}

Let's break down what is happening here.

  1. @ExtendWith(GenesisJunit.class) - using this annotation will enable the Genesis Junit extension. It will manage the Genesis resources during the test.
  2. @CsvData("my-trades.csv") - our test relies on trade data existing in the database. Here we specify that data.
  3. @Inject and SyncEntityDb - all classes available for injection during runtime, are available in our test. Here we're getting a reference to the database.
  4. @Inject and TradeProcessor - we can also inject our own classes. TradeProcessor has a constructor annotated with @Inject, so the dependency injection mechanism knows how to instantiate it.
  5. By the time the test starts, our injected instances are available and we can start testing.
  6. We can access the database as we would normally to get the updated state of the trade.
  7. We verify that the trade status has been updated as expected.

GenesisJunit

GenesisJunit will take care of the Genesis side of the test and will do the following for each test:

  • instantiate the database, including applying the schema
  • start the database, apply the schema/ truncate tables
  • load csv data
  • start a Genesis microservice
  • inject properties into the test class
  • run the test
  • clean up resources

The GenesisJunit class can be found in the global.genesis.testsupport.jupiter package.

Annotations

GenesisJunit supports a range of annotations.

important

You must add @ExtendWith(GenesisJunit::class) or @ExtendWith(GenesisJunit.class) to your class or function. This is essential.

These @ExtendWith(GenesisJunit.class) and all the annotations below work on both the class and method level.

Some of the annotations are Repeatable; this means that you can provide multiple values at the same class or method.

  • If you provide these Repeatable annotations on both the class and method level, both are applied.
  • If you provide non-Repeatable annotations on both the class and method level, the method-level annotation takes precedence.
  • The enable annotations (EnableDataDump and EnableInMemoryTestAuthCache) do not support disabling at the method level once they have been enabled on the class level.

All annotations can be found in the global.genesis.testsupport.jupiter package.

TestScriptFile

Use this annotation to load a script file as part of the test. This annotation is Repeatable.

@ExtendWith(GenesisJunit::class)
@TestScriptFile("my-eventhandler.kts")
internal class ScriptFileExamples {

@Test
fun testScriptFileExamples() {
// just uses "my-eventhandler.kts"
}

@Test
@TestScriptFile("other-eventhandler.kts")
fun testMoreScriptFiles() {
// uses "my-eventhandler.kts" and "other-eventhandler.kts"
}

@Test
@TestScriptFile("other-eventhandler.kts")
@TestScriptFile("more-eventhandler.kts")
fun testManyScriptFiles() {
// uses "my-eventhandler.kts", "other-eventhandler.kts" and "more-eventhandler.kts"
}
}

TestConfigFile

Use this annotation to load a process config file as part of a test. This annotation is not Repeatable.

@ExtendWith(GenesisJunit::class)
@TestConfigFile("my-process-config.kts")
internal class ConfigFileExamples {

@Test
fun myProcessConfig() {
// just uses "my-process-config.kts"
}

@Test
@TestConfigFile("other-process-config.kts")
fun otherProcessConfig() {
// uses "other-process-config.kts"
}
}

CsvData

This annotation is used to load CSV data as part of a test and is also repeatable. Both individual file paths and folder paths are accepted. In terms of individual files, the DumpIt CSV format is supported in both .csv and .csv.gz extensions.

Alternatively, multi-table DumpIt format is supported in the following format:

#TABLE_NAME
COLUMN_NAME1, COLUMN_NAME2, COLUMN_NAME3
COLUMN_VALUE1, COLUMN_VALUE2, COLUMN_VALUE3
COLUMN_VALUE2, COLUMN_VALUE3, COLUMN_VALUE4

So, assuming two tables named FIRST_TABLE and SECOND_TABLE, both with String columns ID and TEXT, both containing 2 records each, a sample file would look like this:

#FIRST_TABLE
ID,TEXT
id1,text1
id2,text2
#SECOND_TABLE
ID,TEXT
id1,text1
id2,text2
@ExtendWith(GenesisJunit::class)
@CsvData("my-data.csv")
internal class CsvDataExamples {

@Test
fun loadCsvData() {
// just uses "my-data.csv"
}

@Test
@CsvData("other-data.csv.gz")
fun loadCsvDataFromOtherDataFile() {
// uses "my-data.csv" and "other-data.csv.gz"
}

@Test
@CsvData("other-data.csv.gz")
@CsvData("data-folder")
fun loadCsvDataFromMultipleFiles() {
// uses "my-data.csv", "other-data.csv.gz" and the "data-folder" folder
}
}

EnableInMemoryTestAuthCache

This annotation enables the in-memory auth cache. To set or remove entity authorization, you will need to inject an InMemoryTestAuthCache into your class. You can apply this annotation on both classes and methods. However, once enabled on the class level, there is no way to disable on the method level.

@ExtendWith(GenesisJunit::class)
@EnableInMemoryTestAuthCache
internal class EnableInMemoryTestAuthCacheExamples {

/**
* inject [InMemoryTestAuthCache] to programmatically change authorisations
*/
@Inject
lateinit var authCache: InMemoryTestAuthCache

@Test
fun testAuthorizeAndRevokeAuthCache() {
// to authorize:
authCache.authorise(
authMap = "TRADE_VISIBILITY",
entityCode = "00000000001TRSP0",
userName = "JohnDoe"
)

// and then to revoke authorisation:
authCache.authorise(
authMap = "TRADE_VISIBILITY",
entityCode = "00000000001TRSP0",
userName = "JohnDoe"
)
}
}

EnableDataDump

This annotation will enable the DATADUMP log level in the microservice. This will output any messages sent and received to the logger. You can apply this annotation on both classes and methods. However, once enabled on the class level, there is no way to disable on the method level.

@ExtendWith(GenesisJunit::class)
@EnableDataDump
class DataDumpTest {

@Test
@EnableDataDump
fun myTest() {
// ...
}
}

RootLogLevel

This annotation will adjust the log level for the life of the test. You can apply this annotation on both classes and methods. Providing this annotation at the method level will take precedence over the class level annotation.

@ExtendWith(GenesisJunit::class)
@RootLogLevel(Level.INFO)
class LogLevelTest {

@Test
fun myTest() {
// root log level set to INFO
}

@Test
@RootLogLevel(Level.TRACE)
fun myOtherTest() {
// root log level set to TRACE
}
}

GenesisHome

Annotation used to specify the Genesis home directory path. It can be applied to classes or functions/methods. If the annotation is not provided, genesis home will default to /genesisHome. Providing this annotation at the method level will take precedence over the class level annotation.

@ExtendWith(GenesisJunit::class)
@GenesisHome("/genesisHomeA")
class GenesisHomeTest {

@Test
fun myTest() {
// this test uses genesisHomeA
}

@Test
@GenesisHome("/genesisHomeB")
fun myOtherTest() {
// this test uses genesisHomeB
}
}

SysDefOverwrite

Annotation used to indicate that a specific system definition property should be overwritten. This annotation can be applied to classes or functions. When specifying this annotation on both the class and the function level, both will be taken into account, unless the same property is overwritten, in which case the method-level annotation will take precedence.

@ExtendWith(GenesisJunit::class)
@SysDefOverwrite("MY_VALUE", "ABC")
internal class OverwriteSysDefTest {

@Test
fun simpleExample() {
// MY_VALUE is set to ABC
}

@Test
@SysDefOverwrite("OTHER_VALUE", "DEF")
fun multipleLevels() {
// MY_VALUE is set to ABC, OTHER_VALUE is set to DEF
}

@Test
@SysDefOverwrite("OTHER_VALUE", "DEF")
@SysDefOverwrite("MY_VALUE", "GHI")
fun overwritingClassLevelSysDef() {
// MY_VALUE is set to GHI, OTHER_VALUE is set to DEF
}
}
tip

When running genesis integration tests, by default, all injecting modules will run on a process named UNIT_TEST_PROCESS. However, in some cases, you may want to override this default process name to use a different one. You can do this by simply overriding the PROCESS_NAME system definition property. The property will be picked up regardless of how you choose to set up your system definitions.

@ExtendWith(GenesisJunit::class)
class MyTest {
@Test
@SysDefOverwrite("PROCESS_NAME", "SOME_CUSTOM_PROCESS_NAME")
fun `test`() {
// Your test code here
}
}

PackageScan

Annotation used to mark classes or functions for package scanning. This annotation should be used to mark classes or functions that require package scanning. It is used to specify the marker interface or class to be used for scanning packages. This annotation is Repeatable.

@ExtendWith(GenesisJunit::class)
@PackageScan(MyClass::class)
internal class PackageScanTest {
@Test
fun myTest() {
// scans the package of MyClass
}

@Test
@PackageScan(MyOtherClass::class)
fun myOtherTest() {
// scans the package of MyClass and MyOtherClass
}

@Test
@PackageScan(MyOtherClass::class)
@PackageScan(MyThirdClass::class)
fun myThirdTest() {
// scans the package of MyClass, MyOtherClass and MyThirdClass
}
}

ProvidedInstance

Annotation used to mark properties or methods as provided instances. Marked properties or methods can be used to override pre defined instances during test runtime.

For example, one could define an interface in production code called MyInterface with a default implementation MyInterfaceImpl.


interface MyInterface

@Module
@ConditionalOnMissingClass(MyInterface::class)
class MyInterfaceImpl: MyInterface

Then we can override the implementation at runtime using a fake/mock implementation:

@ExtendWith(GenesisJunit::class)
internal class ProvidedInstanceTest {

/**
* Will provide [MyClass] as an instance of [MyInterface] during the dependency injection phase.
*/
@ProvidedInstance
private val myInstance: MyInterface = FakeImpl()

@Test
fun myTest() {
//
}
}

UserRight

The UserRight annotation provides users with specific rights during a test.

To use this annotation, annotate any method or class with @UserRight and provide the user and right codes:

@Test
@UserRight("John Doe", "LICENSE_TO_BILL")
fun `user has rights`() {
client.sendRequest(
workFlow = InstrumentDetailsFlow,
request = ByAlternateTypeAlternateCode("RIC", "AMBV4"),
username = "John Doe"
)
}

ExpectedProcessStatus

The ExpectedProcessStatus annotations provide a way to assert the end state of a process after a test. By default, GenesisJunit will assert that the process is healthy (ProcessState.UP).

@Test
@ExpectedProcessStatus(ProcessState.DOWN)
fun `test status DOWN`() {
ProcessStateChangeHandler("test").setProcessDown()
}

Troubleshoot threading and coroutine issues in tests

To help troubleshoot threading and coroutine issues, genesis junit offers support for thread and coroutine dumps on test failure. To enable this, there are three annotations:

  • @ThreadDumpOnFailure - dumps threads on test failure
  • @CoroutinesDumpOnFailure - dumps coroutines on test failure
  • @FullDumpOnFailure - dumps both tests and coroutines on test failure

Component testing

There are two easy ways of testing components in your application:

  • using an API client
  • using the curl tool from the command line

Before you start

Before you start, make sure your server is running. Then run mon to check that your particular component’s process is running. For example, if you want to test one or more requestReply codeblocks (i.e. resources) in your Request Server, check that the application_REQUEST_SERVER process is running.

For any testing, you need to know:

  • the IP address of name of your server
  • the user name and password that will enable you to login and authenticate yourself

Using an API client

This type of software offers an easy way of testing each of your resources.

Two clients that Genesis uses for component testing are:

Broadly speaking, Postman offers more features, but Insomnia is also good and is simpler to use.

Logging on

Whichever client you are using, you need to log in before you can send test requests to the server. This involves two things:

  • providing a SOURCE_REF - this can be any string that identifies all your activity while you are logged in
  • retrieving a SESSION_AUTH_TOKEN, which you can copy and use to authorise all your test requests

For example, to login using Insomnia:

  1. Create a new query in Insomnia.
  2. In front of the url, set the call to POST.
  3. For the url, you need to supply your server instance, then :9064 (in order to send you to the application's Router), and then event-login-auth. For example: https://test-mynewapp:9064/event-login-auth
  4. Set the Body to JSON and insert the message below (substituting your correct user name and password) in the main body.
{
"MESSAGE_TYPE": "TXN_LOGIN_AUTH",
"SERVICE_NAME": "AUTH_MANAGER",
"DETAILS": {
"USER_NAME": "DonJuan",
"PASSWORD": "Password123"
}
}
  1. Click to view the header, then insert SOURCE_REF in the header. For this field, you can use any string that will identify you (in effect). In the example below, we have set SOURCE_REF to BAUDOIN1 (for no particular reason).

  1. When you have done this, click on the Send button.

This returns a set of details in the right side of the Insomnia window, where you can copy the SESSION_AUTH_TOKEN, which you will need for your test requests.

Testing with curl

You can use the curl tool to test a module from the command line.

The simple example below tests the resource event-account-validate-request. It sends a request to see if this resource in the Event Handler is able to validate an account.

curl --request POST 'localhost:9064/event-account-validate-request' \
--header 'SOURCE_REF: 1' \
--header "SESSION_AUTH_TOKEN: $1" \
--header 'Content-Type: application/json' \
-d '{"DETAILS": {"ACCOUNT_ID" : 9 }}'

Now let's look more closely at that.

  • At the beginning, we use the --request parameter, which specifies a custom request, followed by POST to indicate that we are posting data to the resource. The data itself comes later (in the -d parameter).

  • The resource that you are accessing must be part of the URL. In this case, the resource is event-account-validate-request. (Remember that the events you specify are transformed when you generate the objects. for example, EVENT_NAME becomes event-name.)

  • Three --header parameters have been specified:

    • The first header, SOURCE_REF, uniquely identifies your message. In our example, this is simply 1.

    • The SESSION_AUTH_TOKEN is required because, as is usually the case, the resource is non-public; you need to be logged in for access.

    • The Content-Type ... header indicates the media type of the content you are requesting - for example JSON or a png file. This tells the server how to deal with your message. You can find more about content type in html online.

  • The -d parameter specifies the input for the resource. The request itself is always JSON and should always be contained in the DETAILS tag. In this case, we are requesting to validate account id 9.

Now here is a more complex example, which tests the ability to upsert to an eventHandler resource called event-upsert-inventory.

curl --request POST 'myserver-dev-fictional/ws/event-upsert-inventory' \
--header 'SOURCE_REF: 15' \
--header 'Content-Type: application/json' \
--header 'SESSION_AUTH_TOKEN: SnpTI4dvX9gcXDd4BQlOTkp4JSCrXR3t' \
--data-raw '{
"MESSAGE_TYPE": "EVENT_UPSERT_INVENTORY",
"SERVICE_NAME":"FOXTROT_EVENT_HANDLER",
"DETAILS":{
"INSTRUMENT_ID": "8200",
"INSTRUMENT_ID_TYPE": "FOXTROT_ID",
"UNIT_OFFER_PRICE":"102.55",
"UNIT_OFFER_SIZE": "1000000",
"UNIT_BID_PRICE":"100",
"UNIT_BID_SIZE": "2000000",
"PARTIAL_BID_ALLOWED": "TRUE",
"PARTIAL_OFFER_ALLOWED": "TRUE",
"IOI_ID" : "12345",
"TRADER_NAME" : "Ronald.Zappa@madeup.com"
}
}'

Note that the data to be upserted is specified using the --data-raw parameter. This is because the set of fields is reasonably complicated, and it includes an email address - you don't want that @ character to trigger any unwanted processing.

You can find more details about curl parameters online.

The POSTMAN tool has a useful feature tucked away with the icons at the right of the screen. This enables you to translate automatically the query you have built into curl format (or a large number of others).

In the area displayed, just select cURL as the code, and the code for your current query is displayed below for you to copy.