Recently I’ve found myself writing drivers for various programmable cards in a National Instruments PXI-E chassis. This machine will be used to simulate things like digital I/O when testing an embedded hardware system. The two main things that such a driver needs to do is communicate with the hardware it drives correctly and communicate with the ecosystem in which it lives correctly. This post was originally going to be about how to write tests with Fake Function Framework to help write such a driver. It has, however, evolved into a description of the software engineering process / software development methodology that I personally use when writing these drivers.
A major problem I ran into for these drivers is that the hardware that the cards will communicate with isn’t in the lab yet. Because there are several more drivers yet to be written I need to get the current one done before the rest of the equipment arrives. As such I need a solid plan of attack for testing the driver that doesn’t involve this hardware. Coming up with a test plan that properly exercises the code will test whether I understand the requirements for the driver correctly. The major items that need to be tested in this case are:
- Test integration with the rest of the code base: Write unit tests with a fake-ing framework and fake all of the API calls in a way that matches the docs and examples. This will allow the tests to focus on whether the inputs and outputs of its interface matches what the rest of the code base expects without involving the hardware.
- Test that the API is being called correctly: Create a physical loop back between an input and output channel on the hardware. Tests can be written just for the functions that interact with the card’s API to make sure that the value written on the output channel is read on the input channel. This allows us to isolate the interaction with the hardware from the rest of the system.
Where Do We Begin To Develop Something New?
Read the Requirements & Understand the System
While this point might seem self evident it is worth saying out loud: Understanding what the code needs to accomplish must be the first thing that happens; it needs to be done before code of any kind is written. If you don’t understand something about what the requirements ask for make sure that you spend time asking questions. From there, read through the code that you’ll be adding to or interacting with if you aren’t already familiar with it. In my case I’m adding a driver to interact between a simulation and the software that runs on the computers in the plane. This is a large code base and, as such, I’ll be building context about both sides of the software for a long time to come. To gain the appropriate context I spent time seeing how a driver for similar hardware from another vendor works and how it is called.
Read the API Docs
Once I’ve got an idea of what I need to tell the hardware to do I need to find out what part of the API to use and what information the API calls need. Next I can figure out where in our code base to find this information. To the chagrin of all those who like to figure things out for themselves (like me!), the best place to find out how to use APIs is the documentation. Don’t forget the examples for said APIs either. Bear in mind that these documents aren’t always perfect or kept up to date. They’re written by humans just like you and I.
Pro Tip: If your company has purchased the hardware you’re using they’ve likely also purchased a support contract. These support contracts allow you to reach out to the hardware vendor with questions. If the documentation or examples don’t line up with what you’re doing and you can’t find a way to do what you need don’t forget to reach out to the vendor.
Next – Erect the Scaffolding
Through the process of writing several of these drivers I’ve found that, once I know how to tell the hardware to do something with the API, the next two hard parts are:
- Figuring out where to get the information to pass to the hardware
- Figuring out how the driver needs to interact with the rest of the system
Figuring this out comes from reading the code you’ll be integrating with. Once you’ve figured this out the “scaffolding” can be written. I consider this scaffolding things like the class structure, the data types needed to hold the data specific to the driver in question, get/set routines, configuration routines, and any routines or calls required by patterns used in the system. An example of the last item is making sure you register your class with the correct class factory if you’re using factories. Once these things are setup, we can begin writing software based tests.
Begin Writing Unit Tests
Once we’ve got the class structure up and are ready to begin implementing the actual logic of our code it is a good time to start writing unit tests. Yes, this is a very Test Driven Development (TDD) attitude toward development. My personal reason for holding this attitude is: writing a set of tests for a particular function forces me to decide what the inputs and outputs of the function are, how they’ll be passed in and returned, and how errors will be handled.
I have written a lot in the past about unit testing. The long and short of it is that writing tests will make sure you understand the requirements and document what you expect the code to do. Here are a few posts I’ve written on unit testing:
- Unit Testing Will Show You What You Don’t Know
- How To Start Testing An Existing Code Base
- Addressing Arguments Against Unit Testing
Now We Can Implement Some Logic
Once I’m at the point that I’ve got an understanding of how the code communicates, what it’s supposed to do, and got some tests that implement some of the requirements I can actually fill in the logic. For these drivers I usually begin by copying in the hardware API function names. As I start to fill in the parameters to these calls I’ll see what data that I need and start adding the appropriate variables and logic to load the variables with the necessary data if they’re inputs. Once I’ve got all of the parameters filled in to the API calls I’ll fill in the code to handle the output of the API calls. This example I’ve given is very specific to the task I currently find myself handling. You should, however, be able to extrapolate this example to fit what you’re doing.
Don’t Forget To Test The Code As A Whole
After the code compiles and your unit tests pass, don’t forget to do some integration testing. By integration testing I mean making sure that what you’ve written interacts with its environment appropriately. The way that I’ve been doing this with the drivers I’ve written are to use loopbacks. I’ve worked with one of our electrical engineers to tie an input channel to and output channel with physical wiring. This has allowed me to run a simulation and make sure that what my driver outputs is appropriately read in. Without testing like this you can’t know that your code really does what’s intended.
Conclusion
Writing software is hard. Breaking it down into bite sized steps is the best way to get it done. Make sure you get it tested as well. Thanks for joining me on this journey of how I write these drivers!