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