Effective Test Driven Development For Embedded Software
Effective Test Driven Development For Embedded Software
Manuscript submitted April 2006. This paper is based on ideas developed II. CURRENT STATE OF TESTING IN EMBEDDED SOFTWARE &
and work performed for various clients as part of ongoing contract software
development engagements. SHORTCOMINGS
Michael J. Karlesky, William I. Bereza, and Carl B. Erickson, PhD are all
with Atomic Object LLC, 941 Wealthy Street SE, Grand Rapids, MI 49506 A. Ad-hoc Testing
USA. voice: (616) 776-6020 fax: (616) 776-6015
email: [email protected].
Experimentation and ad-hoc testing are often performed
2
during the development process to discover the idiosyncrasies conclusive from an accuracy standpoint and require elaborate
of the system under development. The knowledge gained in test apparatuses. While simulation environments are certainly
these efforts is then applied in the functional source code. necessary for aspects of system testing, a temperature chamber
With ad-hoc testing, test fixtures and experimentation code is not necessary to verify five lines of math code.
used to characterize the system and shape the functional code
are usually discarded or shelved. Over time, these resources III. TEST DRIVEN DEVELOPMENT
fall out of step with system development (or no longer exist at
all) and become vestigial remnants of the system’s evolution. A. Overview
Valuable, executable knowledge (in the form of code) is lost. Test Driven Development inverts the traditional software
Time will almost certainly be lost in later stages of development/test cycle. In TDD, the development cycle is not
development because these tests have not been kept current. a progression of writing functional code and then later testing
it. Instead, testing drives development. A developer looks for
B. Debugging
ways to make the system testable, designs accordingly, writes
Embedded software relies far more heavily on specialized tests and creates testing strategies, and then writes functional
debugging and system inspection tools than does high-level code to meet the requirements of the test-spawned design [3].
software. Most, if not all, of the need for these tools is created Testing takes different forms. At the highest levels (e.g.
by the multivariable equation of hardware and software integration and system testing) full automation is unusual. At
commingling. Bugs may be due to hardware, software, or the lowest level, TDD prescribes fully automated unit testing.
both. Thus, finding the source of unintended behavior In automated unit testing, a developer first writes a unit test
generally requires more effort than in high-level software. (a test that validates correct operation of a single module of
The existence of sophisticated debugging tools in embedded source code, for instance, a function or method [4]) and then
software creates an interesting side effect. With such advanced implements the complementary functional code. With each
debugging tools available (and needed), developers are system feature tackled, unit test code is added to an automated
inclined to design only for debugging and not for true testing. test suite. Full regression tests can take place all the time.
The assumption inherent in design-for-debug is that any and Further higher-level testing will complement these code-
all code is “debuggable.” The limitations in this assumption level tests. Whether this testing is integration or system
are threefold. First of all, undesired behaviors in a system testing, it will generally follow the patterns of traditional
under development can be due to any number of obscure software verification. Ideally, it will also include some
reasons – often in the least expected places. Relying on measure of automation.
design-for-debug is having faith that one is well-capable of Automated unit tests catch the majority of bugs at a low
finding a needle in a haystack. Secondly, debugging sessions level and leave for the human mind difficult testing issues like
are one-time events. After a bug is found and corrected there timing collisions or unexpected sub-system interactions.
is nothing in place to watch that same code and point out TDD provides several clear benefits:
undesired code interaction in the future. Finally, relying 1. Code is always tested.
heavily on debugging rarely enforces good coding practices; 2. Testing drives the design of the code. As a side
debugging can act as a psychological safety net. effect, the code tends to be improved because of
C. Final Testing the decoupling necessary to create testable code.
3. The system grows organically as more knowledge
The traditional “Waterfall” method of software
of the system is gained.
development prescribes a progression of design, build, and test
4. The knowledge of the system is captured in tests;
steps. Final testing is planned as the last major stage of
the tests are “living” documentation.
development and verification before release to production.
5. Developers can add new features or alter existing
Embedded projects, just as high-level software projects, most
code with confidence that automated regression
often follow these same steps.
testing will reveal failures and unexpected results.
Testing planned for the conclusion of a project presents two
problems. First of all, time constraints and budget limitations B. Particular Advantage of TDD in Embedded Software
usually squeeze final testing into a compressed time period or In the context of embedded software TDD provides a
eliminate it entirely. As such, tests that might prevent costly further advantage beyond those already listed. Because of the
future problems are sacrificed for the demands of the present variability of hardware and software during development,
day. Secondly, with testing so removed from development, bugs are due to hardware, software, or a combination of the
source code is unlikely to have been developed for ease of two. With TDD, software bugs can be eliminated to such a
testing. For example, a simple temperature measurement degree that it becomes far easier to pinpoint, by process of
might be implemented such that a code block contains both an elimination, the source of unexpected system behavior (i.e.
analog-to-digital conversion and the math routines that will hardware versus software).
produce a final temperature value. On the surface, there is
nothing wrong with this approach. In final testing, however,
the math of the routine can only be tested by subjecting the
entire system to actual temperature variations or by using a
special voltage simulation rig. These tests are not necessarily
3
composition with mocks of the system. The calls and 1) Automated Unit Testing
parameters of the triad member under test are captured within Developers use MCH to decouple functional logic code
the mocks for test assertions. The proper operation of the logic from hardware code and develop unit tests to be run in an
under test is revealed by its actions on the mocks. automated test framework. These tests are run on-chip, cross-
With mocks constructed for each member of the MCH triad, compiled on a PC, or executed in a platform simulator such
clear testing possibilities become apparent. Code testing via that automated regression tests can always be executed. Note
simulator, on-chip, or in a cross-compiled environment are all that work in this tier can progress without target hardware.
possible. The states and behavior within the Model are tested 2) Hardware Level Testing
independently of hardware events and functional logic. The Developers and engineers use a combination of unit tests,
system logic in the Conductor is tested with simulated events hardware features, and direct developer interaction to test
from Hardware and simulated states in the Model. With a hardware functions and hardware setup code. Using feedback
mock Conductor, even hardware register configuration code loops designed into the hardware, processor diagnostic
and ISR’s can be tested via a simulator, hardware test fixture, functions, hardware test fixtures, and user interaction, all
or board-level feedback loops. MCH code examples follow in hardware functions are tested. The approach taken here is
a later section of this paper. system-dependent. Once hardware functions are tested and
operational, it is likely that tests developed here will be run far
C. Unit Testing Framework
less frequently than in Tier 1.
Unit testing frameworks exist for nearly every high-level 3) Communication Channel Testing
programming language in common use today. The mechanics If the embedded system includes an external
of a test framework are relatively simple to implement [8]. A communication interface, developers use PC tools to exercise
framework holds test code apart from functional code, and capture test results of the system through this channel. A
provides functions for comparing expected and received complementary hardware test fixture, software test fixture,
results from the functional module under test, and collects and and/or significant human interaction are likely to be required
reports the test results for the entire test suite. to exercise the system and provoke communication events.
In our work, we have both customized the open source 4) End to End System Testing
project Embunit (Embedded Unit) and created a very Having confidence in low level test successes, developers
lightweight framework called Unity [9]. Embunit and Unity and/or testers manually exercise an end-to-end exploratory
are both C-based frameworks we modify for target platforms system test looking for emergent timing issues, responsiveness
as needed. deficiencies, UI inconsistencies, etc.
D. The cost of using Model-Conductor-Hardware B. Continuous Integration
MCH adds little to no overhead to a production embedded The technique of continuous integration regularly brings
system. Of course, mocks are not included in the final system. together a system’s code (possibly from multiple developers)
Further, MCH is essentially naming, organization, and calling and ensures via regression tests that new programming has not
conventions with little to no extra memory use or function broken existing programming. Automated build systems allow
calls; any overhead incurred by these conventions is easily source code and tests to be compiled and run automatically.
optimized away by the compiler. These ideas and tools are important supports to effective TDD
TDD, in general, does add to project cost in added but are beyond the scope of this paper [10].
developer time. However, clear savings are realized over the
system’s lifetime in reduced bugs, reduced likelihood of VI. EMBEDDED MODEL-CONDUCTOR-HARDWARE E XAMPLES
recall, and ease of feature additions and modifications.
A. MCH in a C-based Environment
V. TDD IN EMBEDDED SOFTWARE Creating mocks and tests in an embedded C environment is
accomplished through compiled mock.o implementations of
A. Four Tier Testing Strategy header file function declarations. For example, suppose
Thorough software testing includes automated unit testing hardware.h declares all functions for interfacing the
at the lowest level and integration and system testing at higher hardware features of a particular microcontroller. In this
levels. Unit testing was addressed in the preceding section. example, Conductor tests will verify that the Conductor makes
Implementing an overall TDD strategy in embedded software specific calls on the hardware with appropriate parameters. As
is a four tier testing approach. With each step up through the such, a mockhardware.c definition file will be written
tiers, less automated testing occurs and more human containing otherwise empty functions that store individual
interaction is required. However, each tier provides increasing function call parameter values or return specific values – both
test confidence and frees developers and testers to use their as defined by global variables. Object files for mock and
human intelligence and intuition for difficult testing matters functional code are linked together, and tests access the
such as sub-system interaction and timing collisions. previously mentioned global values to verify the Conductor
Automated testing at the lowest levels of system development calls to the mockhardware.h interface.
can eliminate a high number of bugs early on. Ultimately,
system flaws found earlier in the development process cost B. Code Samples
less than those found later. The following code blocks are examples drawn from a real-
5
model.c Hardware_OutputDriveOutputVoltage);
TEST_ASSERT_EQUAL_INT( 78,