Home

Kenny's Blog

08 Jan 2022

Mocking out serial ports

I’ve been hacking around with arduinos and xbees for a while now, and one of the pain points in the past has been figuring out how to test server-side code without needing a physical device.

I was always pretty bad at any sort of software testing before, but since I’ve been working in the software industry for a bit and been exposed to testing at work a lot more I’ve started to understand what options there are.

One option is to mock out the serial class - like the python-xbee library uses for it’s tests:

https://github.com/niolabs/python-xbee/blob/master/xbee/tests/Fake.py

#! /usr/bin/python
"""
Fake.py
By Paul Malmsten, 2010
pmalmsten@gmail.com
Updated by James Saunders, 2016
Inspired by code written by D. Thiebaut
    http://cs.smith.edu/dftwiki/index.php/PySerial_Simulator
Provides fake device objects for other unit tests.
"""


class Serial(object):
    def __init__(self, port='/dev/null', baudrate=19200, timeout=1,
                 bytesize=8, parity='N', stopbits=1, xonxoff=0,
                 rtscts=0):
        """
        Init constructor, setup standard serial variables with default values.
        """
        self.name = port
        self.port = port
        self.timeout = timeout
        self.parity = parity
        self.baudrate = baudrate
        self.bytesize = bytesize
        self.stopbits = stopbits
        self.xonxoff = xonxoff
        self.rtscts = rtscts
        self._is_open = True
        self.fd = 0

        self._data_written = ""
        self._read_data = ""

    def isOpen(self):
        """
        Returns True if the serial port is open, otherwise False.
        """
        return self._isOpen

    def open(self):
        """
        Open the serial port.
        """
        self._is_open = True

    def close(self):
        """
        Close the serial port.
        """
        self._is_open = False

    def write(self, data):
        """
        Write a string of characters to the serial port.
        """
        self._data_written = data

    def read(self, len=1):
        """
        Read the indicated number of bytes from the port.
        """
        data = self._read_data[0:len]
        self._read_data = self._read_data[len:]
        return data

    def readline(self):
        """
        Read characters from the port until a '\n' (newline) is found.
        """
        returnIndex = self._read_data.index("\n")
        if returnIndex != -1:
            data = self._read_data[0:returnIndex+1]
            self._read_data = self._read_data[returnIndex+1:]
            return data
        else:
            return ""

    def inWaiting(self):
        """
        Returns the number of bytes available to be read.
        """
        return len(self._read_data)

    def getSettingsDict(self):
        """"
        Get a dictionary with port settings.
        """
        settings = {
            'timeout': self.timeout,
            'parity': self.parity,
            'baudrate': self.baudrate,
            'bytesize': self.bytesize,
            'stopbits': self.stopbits,
            'xonxoff': self.xonxoff,
            'rtscts': self.rtscts
        }
        return settings

    def set_read_data(self, data):
        """
        Set fake data to be be returned by the read() and readline() functions.
        """
        self._read_data = data

    def get_data_written(self):
        """
        Return record of data sent via the write command.
        """
        return(self._data_written)

    def set_silent_on_empty(self, flag):
        """
        Set silent on error flag. If True do not error.
        """
        self._silent_on_empty = flag

    def __str__(self):
        """
        Returns a string representation of the serial class.
        """
        return "Serial<id=0xa81c10, open=%s>(port='%s', baudrate=%d, " \
            % (str(self._is_open), self.port, self.baudrate) \
            + "bytesize=%d, parity='%s', stopbits=%d, xonxoff=%d, rtscts=%d)"\
            % (self.bytesize, self.parity, self.stopbits, self.xonxoff,
               self.rtscts)

You just end up calling set_read_data and setting it to whatever you want:

        self.device = Serial()
        self.device.set_read_data("test")

The serial object created should fit into any other object that needs it.

Other notes

There are a couple of other options for testing code that interacts with serial ports, which I may write about later:

  • using socat
  • using a pty