BDD for Embedded Systems

Behavior Driven Development for Embedded Systems

Introduction

Behavior-Driven Development (BDD) is a software development methodology that focuses on defining the behavior of a system in plain, human-readable language.

While commonly associated with end user business requirements and web development, it can be applied to embedded systems as well. This includes not only behaviors visible to end users, but also internal behaviors between modules, where one module is the “user” of another module’s behaviors.

BDD can be used across the entire range of embedded system project environments, from exploratory development that starts on the back of a napkin to fully-specified regulated industries like medical, automotive, and aerospace products.

What Is BDD?

BDD is a layer of discipline on top of Test-Driven Development (TDD). It was created by Dan North (see his post Introducing BDD), who found that developers learning TDD would frequently experience confusion and misunderstanding, often leading to poor results applying the methodology.

In the worst case, this would result in projects abandoning TDD, losing all the benefits of an extremely effective technique.

A major source of confusion and misunderstanding is due to the word “test”. That carries a number of connotations, most often that it’s simply a test methodology, and that it should be done by testers, not developers.

While it’s true that TDD uses tests, it’s better thought of as a design and development methodology, driven by developer tests that exercise the code in isolation and verify the results. It allows developers to check their work as they go. The net result is a unit test suite and known-good functioning code, with test results to prove it.

There are also ways to get TDD wrong, primarily by focusing on the internals of the code being developed.

Adding the BDD practices avoids these issues.

BDD replaces the word “test” with the word “behavior” to change the focus from tests and internal implementation, to behavior, i.e. what the code should accomplish regardless of the specific internal implementation. Required behaviors may be known up front or they may emerge as development proceeds. 

BDD still uses tests just like TDD, but with some constraints to maintain the proper focus. The tests verify that the code meets the required behavior.

What Is TDD?

TDD is an iterative methodology used during software development to simultaneously build a unit test suite and the functional code. It’s performed entirely by the developer, not by a separate tester.

The developer performs the following cycle (CUT is the Code Under Test):

  • Write a single new unit test to exercise the CUT before writing the actual code that will pass the test. Thus the test is logically guaranteed to fail initially.
  • Run the current test suite to verify that the new test does indeed fail.
  • Write the code that will pass the test.
  • Run the current test suite to verify that the code changes didn’t break anything unexpectedly, and the new code passes the new test. Revise as necessary until this is true.
  • With a test now showing that the code runs as required, refactor as needed to improve the code, while keeping all the tests passing.

This is known as the Red, Green, Refactor cycle for the colors of a failing test, a passing test, and the refactoring step. The developer repeats the cycle until the code is complete.

While it seems like writing tests and code at the same time would slow development, it actually makes very fast progress as the developer iterates the cycle. In the end, the code is known to be good and doesn’t need time-consuming debugging. This reduction or even total elimination of debugging is a significant net speed-up to the overall project.

TDD tests are run off-target, meaning they are run on the development host or build server, not on the target embedded system. That means they can only test the aspects of the code that are platform-independent (where “platform” is the combination of target hardware, target RTOS or other runtime environment, and target third-party software).

However, this tends to be the bulk of a project’s codebase. That encourages an architecture that segregates the platform-independent components from the platform-dependent components. The firmware crosses that boundary by using thin abstraction layers.

The platform dependencies are simulated via “test doubles” that stand in for the real dependencies during tests.

What Does BDD Add?

These are the BDD constraints, the extra layer of discipline it adds to TDD:

  • Test scenarios that are readable and understandable by non-developers.
  • It exercises the CUT only through its public interface without any knowledge of the internal implementation.
  • It uses a well-defined 3-part test structure; Given-When-Then written in a natural language.
  • It focuses each test on just one specific aspect of behavior.

Up-Front vs. Emergent Requirements

Many embedded systems, particularly in regulated industries, start from up-front requirements. Less formal environments may rely more on emergent requirements. BDD works in all cases to prove that the code meets the final resultant set of functional requirements.

Where requirements are known up front, they can be translated to tests following the BDD constraints. This can help facilitate requirements traceability for auditing and review purposes.

Where requirements emerge as the project progresses, the BDD test names can be compiled into a list of requirements.

By using sentences for test names, the list of test names becomes a requirements document that is understandable to a wider range of technical and non-technical stakeholders. That improves communication to ensure both business and technical considerations are addressed. All stakeholders are able to review and contribute behavioral requirements.

What Are The Benefits of BDD?

There are a number of benefits to BDD, some hinted at above:

  • The resulting code is known to be good, proven to execute correctly by the tests.
  • The resulting test suite serves as a safety net for future development.
  • The test names form a human-readable specification.
  • The test suite acts as executable documentation of how to use the code, automatically updated when the code and tests are updated.
  • When a test fails unexpectedly, it points directly to the problem.
  • Overall development is faster, because fewer bugs escape the developer, resulting in much less debugging and bug fixing.
  • Test coverage is high, often 100% for platform-independent code.
  • The codebase is lean, avoiding bloat, because it only contains code that is required to pass the tests, and the tests are only added for the behaviors that are required.
  • Firmware can be implemented before hardware is available, using test doubles.
  • When failures occur in off-target testing, it’s much easier to investigate them than it is when things fail running on the embedded target.

In this context, test coverage becomes a quality signal (as opposed to a goal metric). If it drops, that’s an indication someone has added code that isn’t covered by a test and may not be a required behavior. That can be checked against the test name list to see if a requirement (and test) needs to be added, or if the code needs to be removed. These offer cross-checking opportunities.

What Are The Limitations Of BDD?

BDD isn’t a magic solution to all problems. Like any tool, it has its limitations. The key is to remember that it’s only one part of a comprehensive development strategy. It’s one of a set of tools.

Any testing takes time to write and maintain. The goal with BDD/TDD is to take small amounts of time doing it in order to save large amounts of time later by avoiding prolonged debugging: detecting, chasing down, isolating, and fixing bugs. It’s a small investment with a big ROI. That’s what James Grenning calls the “Physics of Test Driven Development” vs. the “Physics of Debug Later Programming” (see his article on this).

BDD is an example of the 80/20 rule: it provides the methodology for implementing and verifying 80% of the firmware, and other methods must be used to perform the final 20% (actual projects may see a different specific ratio).

Specifically, the following areas need additional testing and verification methods, because they have to be verified on the specific target platform:

  • Concurrency
  • RTOS interaction
  • Hardware interaction
  • Performance

These are all critically important, and are often mistakenly cited as reasons not to use TDD-based methods such as BDD. However, BDD is still an excellent foundation, providing known good functional components up to the final platform-dependent interaction points.

This is where the test doubles stand in for the final level of target platform interaction, allowing BDD to cover as much of the code path as possible up to the actual concurrency, RTOS, and hardware function call or on-chip register access. Using thin adapter and abstraction layers allows swapping out between the test doubles and the real things. See James Grenning’s article Progress Before Hardware for an overview of this, and his 3-part series Unit testing RTOS dependent code – RTOS Test-Double for an example of one way to do this with RTOS concurrency.

These areas must then be tested further via other methods using on-target testing. Because BDD provides known good functionality, when something fails in this testing, the investigation can focus much more directly on the specifics of the interactions, reducing the problem space.

For example, if concurrent firmware components work properly in a single-threaded BDD context but experience a failure during on-target testing, the problem is most likely related to the concurrency control, such as a mutex not used where it should have been.

Performance is a completely separate domain, because it depends on the entire system, from the target MCU, the on-chip and off-chip peripherals, and PCB design, to all the firmware components built with BDD and integrated from vendors and third parties. Performance and resource utilization need to be measured under real operational conditions on the real system. This is an entirely different testing regime from what BDD covers.

Similarly, there will be other system attributes that can only be verified on the total system with different testing methods, such as security. However, BDD plays an active role in developing the firmware that will be tested for these attributes, since they need to be built in from the ground up. To hear more about this topic, listen for the author of this post, Dojo Five alumni Steve Branam, on The Agile Embedded Podcast.

Do Embedded Firmware Right with Dojo Five on Your Team

Are you looking to modernize your embedded firmware development and support processes? You can book a call with us to get the conversation started.

Discover why Dojo Five EmbedOps is the embedded enterprise choice for build tool and test management.

Sign up to receive a free account to the EmbedOps platform and start building with confidence..

  • Connect a repo
  • Use Dev Containers with your Continuous Integration (CI) provider
  • Analyze memory usage
  • Integrate and visualize static analysis results
  • Perform Hardware-in-the-Loop (HIL) tests
  • Install the Command Line Interface for a developer-friendly experience

Subscribe to our Monthly Newsletter

Subscribe to our monthly newsletter for development insights delivered straight to your inbox.

Interested in learning more?

Best-in-class embedded firmware content, resources and best practices

The AI Productivity Paradox in Firmware Teams

The AI Productivity Paradox in Firmware Teams

Engineering is much more than just generating code quickly An experienced firmware engineer starts leaning on AI every day. Tickets move faster. The empty-file dread is gone. “Honestly, I’m at least 30% quicker now,” they tell their manager. Three months later, the graphs don’t share the enthusiasm. Incidents per PR are up. Review queues are sticky. QA is catching regressions in places that used to be quiet. Nobody can point to a single catastrophic failure, but the background noise level

Read More »
A bike chain close up

Choosing a Continuous Integration (CI) compiler

Choosing the right Continuous Integration (CI) compiler is crucial because it impacts code quality, deployment reliability, and development velocity. A consistent compiler version and configuration ensure that the same source code always produces identical binaries and avoid the “works on my machine” scenarios. A wisely chosen compiler is future-proofing and assuring that it can always be obtainable, which can avoid spending extra time revisiting the CI when adding a new feature in the future. We wanted to set up CI

Read More »
Futuristic data tube

Choosing an embedded development board

Choosing a first development board can be daunting with so many choices. Here are some examples to give you an idea of what you could consider for your first board. And as always, if you have a question – reach out to [email protected]. There is a community here to help you out. Lots of options in the development board arena This previous blog covered a number of factors to consider when choosing a first development board. Here are some examples

Read More »