Got an Integration Test for that, Johnny Cash?

There is a great song by Johnny Cash called “One piece at a time.”

The song is a story of a man who moves to Detroit to work in a factory that makes Cadillacs. He has always wanted a Cadillac, and so he comes up with a plan to make one with parts left over at the plant. It takes years, but he finally has enough pieces and he tries with his friends to “put it together one night”. (We’re gonna just go with it…)

Well, since he has been taking the pieces over time he runs into a slight problem:

“The transmission was a 53
And the motor turned out to be a 73
And when we tried to put in the bolts all the holes were gone.”

Johnny is facing a classic problem in software engineering… and one that we will quickly run into when we develop solutions relating to IoT: integration.

Fortunately, we have tools and techniques to help us out with this situation.

Let’s take a look at a small bit of our the DHT monitor code that runs on the Raspberry Pi….

humidity, temperature = self.hardware.readDhtValues()
if humidity is not None and temperature is not None:
 url = self.utils.GetUrl(monitorName, temperature, humidity)
 self.network.sendWeb(webapp, url)
 self.network.sendBBT(humidity, temperature)
...

This code gets temperature and humidity measurements from the DHT22 hardware. If the measurements are “good”, it then formats a part of a URL based on those values. We then hand off the portion of a URL to a piece of code that sends it to the DHT web app (refer to earlier posts in this blog) running on Linux in the Microsoft Azure cloud.) It also sends the data to Beebotte through another network call.

Last time we wrote a unit test for the getUrl function that creates the URL fragment. We also hopefully have tests for the sendWeb and sendBBT functions. The thing that I would like to explore today is making sure that the 4 main chunks of code here (hardware reading, string formatting, calling our custom app, and calling beebotte.com) all work
together.

To do this, we need to do a few special things. In this integration test, we will NOT test getting readings from the DHT. We will also NOT test if the code that sends data over the internet to our custom app works correctly or if the code from the beebotte libraries works as expected.

What we are testing here is that in the lingo of Johnny’s song, the holes all lineup.

To make things testable, we will need to continue to refactor and reorganize our code. This is a wonderful side effect of testing, by the way. I simply cannot say this enough. The act of writing tests for code results in the code being better organized and easier to understand. To help convey the point, a picture might help.

New program structure:


Here is the test code:
 

import unittest
from DhtmonitorApp import DhtmonitorApp
from DhtNetwork import DhtNetwork
from DhtHardware import DhtHardware
import logging
import sys
from mock import MagicMock
class DhtmonitorIntegrationTest(unittest.TestCase):
   def test(self):
      self.assertTrue(True)
   def test_main_function(self):
      hardware = DhtHardware()
      hardware.readDhtValues = MagicMock(return_value=(10.1, 20.2))
      hardware.initialize = MagicMock(return_value=True)
      network = DhtNetwork()
      network.sendWeb = MagicMock(return_values=True)
      network.sendBBT = MagicMock(return_values=True)
      dht = DhtmonitorApp()
      dht.hardware = hardware
      dht.network = network
      dht.execute( "webapp", "sensor")
      network.sendWeb.assert_called_with("webapp", "/dhtmonitor/temperature/20.2/humidity/10.1/sensor/sensor" )
      network.sendBBT.assert_called_with(10.1, 20.2)

if __name__ == ‘__main__’:
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
stream_handler = logging.StreamHandler(sys.stdout)
logger.addHandler(stream_handler)
logger.info(‘Started logging’)
unittest.main()

In this code, line 2 includes a reference to the main class that we want to test. We include in lines 3 and 4 the modules that get called by our main class. The reason that we include them is so that we can mock out their behavior. We will see that in a moment. I would like to mention that I have made the choice to NOT mock out the Utils call that formats the URL simply because it is a pretty small piece of code. By not mocking out the Utils module, we are setting ourselves up for duplicating some of the testings that we did with our earlier unit test. I typically don’t like to double cover when it comes to tests. In this situation, I think it is not that big of a deal. The main downside to doing this is that we may have to fix 2 tests rather than one at some point.

In lines 15-20 you can see that we are using python’s “MagicMock” as a mechanism for mocking out the calls to our network and hardware modules. There is another “trick” that we will need to do in those modules as well, but we will get to that in a minute.

It is important to remember that the above code is an integration test. This test is a separate program and it references our application code. That may be obvious, but it is worth repeating because it is a very important point.
Let’s revisit our diagram and this time actually include our integration test and our mocks…

 


Take a look at lines 24, 25 and 26 of the code example. Up to this point in the test, we have basically been setting things up. Line 24 kicks off the processing, and lines 25 and 26 asserts that our expected calls get made with the appropriate data to our networking module.

This brings up another important part of testing. When you test, you should assert that something happened. It may be tempting to just end the test at line 24 and assume that if no exceptions were thrown, and the test ran, that everything is OK. That is a little bit like saying that just because a piece of code compiles, it does everything that we want it to
do. In fact, lines 25 and 26 probably don’t go far enough. It makes a great deal of sense to write more tests to help determine what happens if things don’t go as well as planned. For instance, what happens if we don’t get any readings (or get an exception) from the DHT. What happens if we get an error sending data to our custom app, or to beebotte.com, or both. You get the idea. It is easy to see how writing tests not only helps us make sure that we have written our code right, but also that we have written the right code.

Here is another great resource for python mocking. http://www.drdobbs.com/testing/using-mocks-in-python/240168251

There is one additional thing that I would like to mention, and it has to do specifically with our hardware module, “DhtHardware.py”. In that module, I am importing code that is specifically used to interface with the Raspberry Pi and DHT22 hardware. I’d rather not install those python libraries on my Mac, and it turns out that we don’t need to. This is great, because it also means that we won’t need to worry about installing those hardware libraries in our continuous integration system either.

We will get to CI later in this series. For now, let’s take a look at a technique that I am using to conditionally import the libraries for the DHT22, as well as the libraries that allow access to the GPIO pins on the Raspberry Pi.

DhtHardware.py

import datetime
import os

if (os.environ.get("dhtenv")=="prod"):
 import RPi.GPIO as GPIO ## Import GPIO Library
 import Adafruit_DHT

class DhtHardware:

 outPin = 11
 inPin = 4

 def initialize(self):
 GPIO.cleanup()
 GPIO.setmode(GPIO.BOARD) ## Use BOARD pin numbering
 GPIO.setup(self.outPin, GPIO.OUT) ## Setup GPIO pin 11 to OUT

 def readDhtValues(self):
 humidity, temperature = Adafruit_DHT.read_retry( Adafruit_DHT.DHT22, self.inPin )
 return (humidity, temperature)

Note the code on line 4.  This code checks to see if I have an OS environment variable called “dhtenv” set to “prod”.  If I don’t have this set, the tests and code can avoid all kinds of problems by not importing the PRI.GPIO and Adafruit_DHT libraries.  I will need to remember to set these environment variables on the Raspberry Pi devices when I run the code there.

It is worth mentioning that it is possible to Mock out backend calls in other languages as well (Java, C/C++, .NET C# etc.)  This is a very valuable tool when it comes to developing code for IoT specific hardware.

This blog post has been pretty technical.  To sum up, the main points are:

  • Integration testing for IoT devices is not only practical but a very valuable tool.
  • The main point of integration testing is to make sure that our “unit” code all integrates together.  Integration testing will also force us to organize our code better.
  • When integration testing, it is important to not only exercise the “system under test”, but also to assert what we expect to happen. This applies to unit testing as well.
  • When integration testing, we don’t want/need to test all the way down into our “unit” code.  In fact, sometimes that code is hardware specific, and it is necessary to mock out its functionality.
  • It is possible to use environment variables (at least on embedded Linux based systems) to help our tests and code to determine if they are running on a test system or production hardware.
I hope you have enjoyed this post.  If you get nothing else from this, I’d also recommend that you pick up a copy of Johnny Cash’s 16 Biggest Hits.
It makes for great listening on Friday nights!
Happy Weekend!

 

 

Author: Miles Porter

Miles Porter is a senior consulting software engineer in the Minneapolis, MN. He is interested in pragmatic software development practices, embedded software, and cloud computing. In his free time he likes to travel with his family, play upright and electric basses and study Taekwondo.

Leave a comment