Skip to main content

Tool for testing FIX (Financial Information eXchange protocol) services

Project description

The purpose of this tool is to provide a way to test networking components using the FIX level at the system level, not unit test. Initially, I wanted a way to reproduce and document specific test cases so that I could perform regression tests at a later date.

This tool provides a way of creating test cases that can act as FIX clients or as FIX servers. But this is not a simulator, the test case author is responsible for generating the actual messages and checking their correctness.

What this is not

  • This is not a simulator. This tool was made to help document specific test cases (thus ensuring that I could repro and verify a test case).

  • This is not meant for unit testing, but component level testing.

  • This currently only supports FIX-based protocols. In theory this could support other protocols, but I haven’t tried to make it more protocol agnostic.

What is supported

  • Groups

  • Binary fields

  • TestRequest/Heartbeat processing

How to use

  1. Write a configuration file

  2. Write a testcase

  3. Run the testcase

Configuration

The configuration is gathered from a config file. The default name for the file is my_config.py. A full description of the contents of the file may be found in sample_config.py

Here is a sample configuration file.

ROLES = {
    'client': {},
    'test-server': {},
}

FIX_4_2 = {
    # The values here are examples only.  They should be customized
    # for your particular needs/implementation.
    'protocol_version': 'FIX.4.2',
    'header_fields': [8, 9],
    'binary_fields': [],
    'required_fields': [8, 9, 10, 35],
    'group_fields': {},
    'max_length': 2048,
}

CONNECTIONS = [
    {
        # connection information
        'name': 'client-FIX-test-server',
        'protocol': 'FIX',
        'host': 'localhost',
        'port': 9000,

        'client': 'FixClient',
        'test-server': 'FixServer',
        'acts-as-server': 'test-server',

        # protocol information
        'protocol_version': FIX_4_2['protocol_version'],
        'binary_fields': FIX_4_2['binary_fields'],
        'header_fields': FIX_4_2['header_fields'],
        'required_fields': FIX_4_2['required_fields'],
        'group_fields': FIX_4_2['group_fields'],
    },
]

Sample test code

Here is an example of a test. This test sends a logon message and then a logout message. In this case, the test tool is running as a server and a client (thus all messages are logged twice, once on the sending side and once on the receiving side).

In a more typical test case, this code would be hidden inside of a base class. Logon/logout are usually performed within the setup/teardown rather than part of the test proper.

import logging
import time

from fixtest.base.asserts import *
from fixtest.base.controller import TestCaseController
from fixtest.base.queue import TestInterruptedError
from fixtest.base.utils import log_text
from fixtest.fix.constants import FIX
from fixtest.fix.messages import logon_message, logout_message
from fixtest.fix.transport import FIXTransportFactory


class LogonController(TestCaseController):
    """ The base class for FIX-based TestCaseControllers.

        This creates a client and a server that will
        communicate with each other.  So they will use
        the same link config.
    """
    def __init__(self, **kwargs):
        super(LogonController, self).__init__(**kwargs)

        self.testcase_id = 'Simple-1'
        self.description = 'Test of the command-line tool'

        config = kwargs['config']

        self.server_config = config.get_role('test-server')
        self.server_config.update({'name': 'server-9000'})

        self.server_link_config = config.get_link('client', 'test-server')
        self.server_link_config.update({
            'sender_compid': self.server_link_config['test-server'],
            'target_compid': self.server_link_config['client'],
            })

        self.client_config = config.get_role('client')
        self.client_config.update({'name': 'client-9000'})

        self.client_link_config = config.get_link('client', 'test-server')
        self.client_link_config.update({
            'sender_compid': self.client_link_config['client'],
            'target_compid': self.client_link_config['test-server'],
            })

        self._servers = dict()
        self._clients = dict()

        factory = FIXTransportFactory('server-9000',
                                    self.server_config,
                                    self.server_link_config)
        factory.filter_heartbeat = False

        server = {
            'name': 'server-9000',
            'port': self.server_link_config['port'],
            'factory': factory,
        }
        self._servers[server['name']] = server

        # In the client case we do not need to provide a
        # factory, Just need a transport.
        client = {
            'name': 'client-9000',
            'host': self.client_link_config['host'],
            'port': self.client_link_config['port'],
            'node': factory.create_transport('client-9000',
                                            self.client_config,
                                            self.client_link_config),
        }
        self._clients[client['name']] = client

        self._logger = logging.getLogger(__name__)


    def clients(self):
        """ The clients that need to be started """
        return self._clients

    def servers(self):
        """ The servers that need to be started """
        return self._servers

    def setup(self):
        """ For this case, wait until our servers are all
            connected before continuing with the test.
        """
        # at this point the servers should be waiting
        # so startup the clients
        self.wait_for_client_connections(10)
        self.wait_for_server_connections(10)

    def teardown(self):
        pass

    def run(self):
        """ This test is a demonstration of logon and
            heartbeat/TestRequest processing.  Usually
            the logon process should be done from setup().
        """
        client = self._clients['client-9000']['node']
        client.protocol.heartbeat = 5
        # We only have a single server connection
        server = self._servers['server-9000']['factory'].servers[0]
        server.protocol.heartbeat = 5

        # client -> server
        client.send_message(logon_message(client))

        # server <- client
        message = server.wait_for_message(title='waiting for logon')
        assert_is_not_none(message)
        assert_tag(message, [(35, FIX.LOGON)])

        # server -> client
        server.send_message(logon_message(server))
        server.start_heartbeat(True)

        # client <- server
        message = client.wait_for_message(title='waiting for logon ack')
        client.start_heartbeat(True)
        assert_is_not_none(message)
        assert_tag(message, [(35, FIX.LOGON)])

        # Logout
        client.send_message(logout_message(client))
        message = server.wait_for_message(title='waiting for logout')
        assert_is_not_none(message)
        assert_tag(message, [(35, FIX.LOGOUT)])

        server.send_message(logout_message(server))
        server.start_heartbeat(False)

        message = client.wait_for_message('waiting for logout ack')
        client.start_heartbeat(False)
        assert_is_not_none(message)
        assert_tag(message, [(35, FIX.LOGOUT)])

Running the test

To run this, use the command line

fixtest -c simple_config.py testcases/logon_test.py

Sample output

(fixtest)~/dev/src/fixtest > fixtest -c simple/simple_config.py simple/logon_controller.py
12:52:10.468172: ================
12:52:10.468496: Starting test: 2014-08-06
12:52:10.468643:   Module: simple/logon_controller.py
12:52:10.468778:   Controller: LogonController
12:52:10.468908:   Config: simple/simple_config.py
12:52:10.470420:
12:52:10.470547:   Test case: Simple-1
12:52:10.470657:   Description: Test of the command-line tool
12:52:10.470761: ================
12:52:10.470868: server:server-9000 starting on port 9000
12:52:10.472101: fixtest.fix.transport: server:server-9000 listening on port 9000
12:52:10.472997: client:client-9000 attempting localhost:9000
12:52:12.810111: client-9000: Connection made
12:52:12.810329: fixtest.fix.transport: client:client-9000 connected to localhost:9000
12:52:12.810626: Connected: fixtest.fix.transport.FIXTransportFactory : server-9000
12:52:12.811074: server-9000: Connection made
12:52:13.010270: client-9000: message sent
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixClient, 56=FixServer, 98=0, 108=5, 34=1, 52=20140806-12:52:13, 10=045

12:52:13.012275: server-9000: message received
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixClient, 56=FixServer, 98=0, 108=5, 34=1, 52=20140806-12:52:13, 10=045

12:52:13.015563: server-9000: message sent
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixServer, 56=FixClient, 98=0, 108=5, 34=1, 52=20140806-12:52:13, 10=045

12:52:13.016854: client-9000: message received
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixServer, 56=FixClient, 98=0, 108=5, 34=1, 52=20140806-12:52:13, 10=045

12:52:13.017925: client-9000: message sent
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixClient, 56=FixServer, 34=2, 52=20140806-12:52:13, 10=053

12:52:13.019156: server-9000: message received
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixClient, 56=FixServer, 34=2, 52=20140806-12:52:13, 10=053

12:52:13.020144: server-9000: message sent
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixServer, 56=FixClient, 34=2, 52=20140806-12:52:13, 10=053

12:52:13.021321: client-9000: message received
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixServer, 56=FixClient, 34=2, 52=20140806-12:52:13, 10=053

12:52:13.022400: server-9000: Connection lost
12:52:13.022687: client-9000: Connection lost
12:52:13.023373: ================
12:52:13.023508: Test status: ok

More sample code

This is a sample of what the code would like if the logon/logout code were removed and placed in the base class setup/teardown functions.

Thus leaving run() to perform the real test work.

import logging

from fixtest.base.asserts import *
from fixtest.fix.constants import FIX
from fixtest.fix.messages import new_order_message, execution_report

from simple_base import BaseClientServerController


class SimpleClientServerController(BaseClientServerController):
    """ The base class for FIX-based TestCaseControllers.
    """
    def __init__(self, **kwargs):
        super(SimpleClientServerController, self).__init__(**kwargs)

        self.testcase_id = 'Simple NewOrder test'
        self.description = 'Test of the command-line tool'

        self._logger = logging.getLogger(__name__)

    def run(self):
        """ Run the test.  Here we send a new_order and
            then a modify.
        """
        # client -> server
        self.client.send_message(new_order_message(self.client,
            symbol='abc',
            side='0',
            order_type='1',
            extra_tags=[(38, 100),      # orderQty
                        (44, 10),       # price
                    ]))

        # server <- client
        message = self.server.wait_for_message('waiting for new order')
        assert_is_not_none(message)

        # server -> client
        self.server.send_message(execution_report(self.server,
            message,
            exec_trans_type='0',
            exec_type='0',
            ord_status='0',
            symbol='abc',
            side='0',
            leaves_qty='100',
            cum_qty='0',
            avg_px='0'))

        # client <- server
        message = self.client.wait_for_message('waiting for new order ack')
        assert_is_not_none(message)

Here is the resulting output:

17:48:15.066436: ================
17:48:15.066607: Starting test: 2014-08-07
17:48:15.066684:   Module: simple/simple_test.py
17:48:15.066757:   Controller: SimpleClientServerController
17:48:15.066827:   Config: simple/simple_config.py
17:48:15.067619:
17:48:15.067700:   Test case: Simple NewOrder test
17:48:15.067772:   Description: Test of the command-line tool
17:48:15.067841: ================
17:48:15.067912: server:server-9940 starting on port 9940
17:48:15.068883: fixtest.fix.transport: server:server-9940 listening on port 9940
17:48:15.069471: client:client-9940 attempting localhost:9940
17:48:15.112361: Connected: fixtest.fix.transport.FIXTransportFactory : server-9940
17:48:15.112912: server-9940: Connection made
17:48:15.113281: client-9940: Connection made
17:48:15.113377: fixtest.fix.transport: client:client-9940 connected to localhost:9940
17:48:15.270715: client-9940: message sent
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixClient, 56=FixServer, 98=0, 108=5, 34=1, 52=20140807-17:48:15, 10=058

17:48:15.271588: server-9940: message received
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixClient, 56=FixServer, 98=0, 108=5, 34=1, 52=20140807-17:48:15, 10=058

17:48:15.272481: server-9940: message sent
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixServer, 56=FixClient, 98=0, 108=5, 34=1, 52=20140807-17:48:15, 10=058

17:48:15.273204: client-9940: message received
    Logon : 8=FIX.4.2, 9=68, 35=A, 49=FixServer, 56=FixClient, 98=0, 108=5, 34=1, 52=20140807-17:48:15, 10=058

17:48:15.274292: client-9940: message sent
NewOrderSingle : 8=FIX.4.2, 9=139, 35=D, 49=FixClient, 56=FixServer, 11=client-9940/20140807/1, 21=1, 55=abc, 54=0,     60=20140807-17:48:15, 40=1, 38=100, 44=10, 34=2, 52=20140807-17:48:15, 10=105

17:48:15.275188: server-9940: message received
NewOrderSingle : 8=FIX.4.2, 9=139, 35=D, 49=FixClient, 56=FixServer, 11=client-9940/20140807/1, 21=1, 55=abc, 54=0,     60=20140807-17:48:15, 40=1, 38=100, 44=10, 34=2, 52=20140807-17:48:15, 10=105

17:48:15.276382: server-9940: message sent
ExecutionReport : (New) : 8=FIX.4.2, 9=224, 35=8, 49=FixServer, 56=FixClient, 11=client-9940/20140807/1, 21=1, 55=abc, 54=0, 60=20140807-17:48:15, 40=1, 38=100, 44=10, 34=2, 52=20140807-17:48:15, 37=server-9940/20140807/2, 17=server-   9940/20140807/1, 20=0, 150=0, 39=0, 151=100, 14=0, 6=0, 10=176

17:48:15.277720: client-9940: message received
ExecutionReport : (New) : 8=FIX.4.2, 9=224, 35=8, 49=FixServer, 56=FixClient, 11=client-9940/20140807/1, 21=1, 55=abc, 54=0, 60=20140807-17:48:15, 40=1, 38=100, 44=10, 34=2, 52=20140807-17:48:15, 37=server-9940/20140807/2, 17=server-   9940/20140807/1, 20=0, 150=0, 39=0, 151=100, 14=0, 6=0, 10=176

17:48:15.280502: client-9940: message sent
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixClient, 56=FixServer, 34=3, 52=20140807-17:48:15, 10=067

17:48:15.281089: server-9940: message received
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixClient, 56=FixServer, 34=3, 52=20140807-17:48:15, 10=067

17:48:15.282183: server-9940: message sent
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixServer, 56=FixClient, 34=3, 52=20140807-17:48:15, 10=067

17:48:15.282773: client-9940: message received
    Logout : 8=FIX.4.2, 9=57, 35=5, 49=FixServer, 56=FixClient, 34=3, 52=20140807-17:48:15, 10=067

17:48:15.284066: server-9940: Connection lost
17:48:15.284246: client-9940: Connection lost
17:48:15.284561: ================
17:48:15.284648: Test status: ok

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fixtest-0.1.0.tar.gz (33.6 kB view details)

Uploaded Source

File details

Details for the file fixtest-0.1.0.tar.gz.

File metadata

  • Download URL: fixtest-0.1.0.tar.gz
  • Upload date:
  • Size: 33.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for fixtest-0.1.0.tar.gz
Algorithm Hash digest
SHA256 64055109442fc99b665c65e648c09e615473d0bb22e46303d9dcaae163838bb4
MD5 04cae30e4eea5defb869dd6a2cd8fce8
BLAKE2b-256 c55ad1294c3952a3b776278b8bc93f94fb441efee5a91b97d911e442aa4d6e8a

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page