Why TDD Fails in Embedded Teams—and How to Fix It
At first glance, adopting Test-Driven Development (TDD) for embedded systems development appears to make a lot of sense. Create a test list for the component you’re working on and then let the tests drive the development of your production code. Not only do you write proven code, but you also have a reservoir of tests that can be executed for integration tests to ensure that everything is working as expected.
But there’s a problem! Despite the sound logic behind Test-Driven Development, many teams and engineers I speak with have tried to adopt it but have failed miserably. Instead, they’ve reverted to the old code-first, test-later process, which slows down development and increases technical debt and risk.
If Test-Driven Development has numerous benefits for software teams, why does it seem to be failing when embedded systems teams attempt to adopt it?
In this post, I will explore the top three reasons I see teams fail when they attempt to adopt Test-Driven Development. I’ll also provide you with a few recommendations on how to successfully adopt TDD and reap all the benefits it has to offer.
Reason TDD Adoption Fails #1 – The Process Feels Awkward
When I first started working with Test-Driven Development over a decade ago, I disliked it. I understood the theory and how it was supposed to work. All the benefits that were just beyond my fingertips. Benefits like:
- Catching bugs the moment I create them
- Less time spent debugging (an activity I absolutely loathe)
- Higher code quality
- Tests for every production line of code
- Automated regression testing
I wanted those benefits, but the process just felt awkward! It went against nearly every mental process I had groomed for how to write software.
TDD sounded so simple. First, create a list of tests that would be needed to test my component. Next, follow the TDD Microcycle (As defined by James Grenning):
- Add a small test.
- Run all the tests and see the new one fail, maybe not even compile.
- Make the small changes needed to pass the test.
- Run all the tests and see the new one pass.
- Refactor to remove duplication and improve expressiveness.
The problem was that I could see where I wanted the code to go! But I couldn’t go there because I didn’t have a test yet! I kept jumping the gun and writing production code that wasn’t needed yet. I wanted to quit and return to the way I had always written code.
That’s where I discovered how to overcome this reason for failing to adopt TDD.
When you first adopt Test-Driven Development (TDD), the process will be awkward. It’s going to feel weird and challenge the way you think about writing software. The solution is simple. Keep practicing the techniques!
Before I adopted TDD, I had spent over a decade grooming thought patterns and ways of writing software that my brain wasn’t going to just give up on overnight! They had worked for me up to this point, but things had to change.
So, I pushed through. I used TDD for every line of code I wrote for over 6 months. No matter what. By the end of the six months, when I tried to write code without writing a test, it just felt wrong. It was awkward. I asked how I could write code without writing a test first.
Adopting TDD is going to feel awkward. Your brain will push back because it’s a different way of doing things. It’ll tell you that this is slowing you down, and it might be at first because your brain is forming new neural pathways as it learns the TDD process. In the end though, you’ll find that using TDD helps you write production code far faster and cheaper than you would have otherwise thought!
Reason TDD Adoption Fails #2 – There’s “No Time”
One of the great reasons (and/or excuses) why too many teams fail to adopt TDD is that there isn’t enough time. A deadline is looming, or management is putting pressure on the team to deliver by a specific date. Or perhaps, they just need to “get it done” now and they’ll go back and do it “the right way” later.
The problem with failing to adopt TDD because there isn’t time is that you’ll never be able to write high-quality firmware quickly if you don’t use the right processes and techniques that get you there.
TDD is peculiar in that it feels like you’re going slow. You need to write a test that fails first. You can’t write more code than a test calls for, only enough to make it pass. With the pressure on, there’s no time for this slow and steady pace! The code just needs to be banged out, and you need to get on to the next thing.
But here’s the thing. When you write that test and then just the production code, you’re not coding slower. You’re writing code that is already tested, saving time and money on the backend of the project by “slowing” down the front-end.
The slow pace up front isn’t actually slow. It’s the fastest way you can write production code that minimizes:
- Time spent in QA testing
- Time spent debugging
- Time finding bugs in code you haven’t looked at in months
TDD helps you to elevate the quality of your software and eliminate the waste that every development cycle has in debugging and testing your code.
That’s the trick! If you are serious about adopting TDD, you must recognize that you have to go slow to go fast. The process will feel like you’ve slowed down, but you’re actually going faster!
You need to shift your mindset and recognize that the code might initially seem like it takes longer to deliver, but you’re not just delivering untested code; you’re delivering production-ready and tested code.
That’s a big difference.
Reason TDD Adoption Fails #3 – Failing to Abstract the Hardware
Adopting TDD within embedded teams can be a struggle if you are used to writing firmware like it’s 1999. In other words, you tightly couple your application code to your hardware.
The problem with TDD and embedded systems is that firmware runs on hardware. That means there are calls into ADC drivers, DMA controllers, and a plethora of other hardware devices.
How can you possibly adopt TDD if you have to run your test harnesses on hardware or mock out the hardware? That’s a highly complex activity that is time-consuming and potentially quite costly!
There is a simple trick that I’ve come to use for the majority of embedded projects that I work on. I abstract the hardware and only use TDD on the application code!
I know! This may sound blasphemous to you if you are a TDD purist. Shouldn’t all lines of code have test cases? I suggest that the answer is no. You only need to write tests for the code that makes the most sense to invest time and money on. In most cases, that is the application code and not the low-level firmware.
That doesn’t mean you should never write test cases for low-level drivers. You just need to write the tests if it’s critical to your product or you don’t trust the drivers that the microcontroller vendor provides to you.
The key for embedded teams interested in successfully adopting Test-Driven Development (TDD) is to limit where you adopt it. Keep the focus on your application code. Abstract out your hardware. If you break your architecture up into a high-level application that through an abstraction interacts with your real-time firmware, the hardware, adopting TDD will be much easier.
Then you can easily run your test cases on a host machine without needing your target hardware. In fact, you’ll be opening up your architecture and system to adopting several other modern techniques, such as simulation and CI/CD.
The Bottom Line
Adopting Test-Driven Development can be a game changer for embedded software teams, if you avoid the common reasons adoption fails. The biggest reasons for failure I’ve encountered have been:
- The process feels awkward
- There isn’t time
- The hardware isn’t abstracted
There are certainly many others like:
- Concerns about fitting legacy code into the process
- Lack of buy in from management or the development team
- Self-taught without expert training
- No external coach to hold you accountable
Through my own development experiences and those with my clients, I’ve seen the power of Test-Driven Development.
Ultimately, that power requires discipline and a desire to become a better developer or a more effective team. You must want to raise the bar on your software quality and spend less time debugging.
We’ve explored the most common reasons why adopting Test-Driven Development (TDD) fails for embedded software teams. I’ve made a few suggestions on how you can avoid these failures. It’s now up to you to follow them and join the elite developers who leverage TDD to develop faster, smarter firmware.
You have to go slow to go fast. TDD feels slow, but it’s the fastest way to deliver quality code.
Struggling to keep your development skills up to date or facing outdated processes that slow down your team, raise costs, and impact product quality?
Here are 4 ways I can help you:
- Embedded Software Academy: Enhance your skills, streamline your processes, and elevate your architecture. Join my academy for on-demand, hands-on workshops and cutting-edge development resources designed to transform your career and keep you ahead of the curve.
- Consulting Services: Get personalized, expert guidance to streamline your development processes, boost efficiency, and achieve your project goals faster. Partner with us to unlock your team's full potential and drive innovation, ensuring your projects success.
- Team Training and Development: Empower your team with the latest best practices in embedded software. Our expert-led training sessions will equip your team with the skills and knowledge to excel, innovate, and drive your projects to success.
- Customized Design Solutions: Get design and development assistance to enhance efficiency, ensure robust testing, and streamline your development pipeline, driving your projects success.
Take action today to upgrade your skills, optimize your team, and achieve success.