Applying BDD & TDD in legacy¶
As I described in Introducing BDD & TDD, understanding the common goals of Behavior & Test Driven Development
and defining your specific needs is the first and most critical step to start using this essential discipline.
Whenever the goal is to use new tools, it is simple: purchase them, shop for some hands-on training, and you are done.
Our ambition is different: we want to become Agile and Lean.
Then, it depends on the starting point too. B&TDD in greenfield development is relatively simple: start writing your tests and code!
However, starting with B&TDD in (Modern) Embedded Software can be more challenging due to the existing and often
extensive codebase that was developed before the emergence of B&TDD.
Then, the question ‘How to start?’ is more like ’How to get out of the waterfall?’
How to start?¶
With millions of lines of existing code, a requirement such as ‘Apply TDD (or/or BDD) everywhere’ isn’t realistic.
One needs realistic goals as a point on the horizon and some derived ones for the short term. And one should
constantly measure whether you have reached them and update the short-term goals.
Sound familiar? That is essentially TDD!
A requirement like: ‘For every iteration (sprint, release, …), the test coverage should grow’ is more realistic. And is
relatively easy to measure.
I like to extend that requirement in two ways:
Such that is valid for all levels in the ‘V’: for units (unit tests), modules, and the system (acceptance tests)
That only automated tests count.
I deliberately demand more coverage only, not that (all) new code is developed with Behavior or Test Driven
As B&TDD is a simple implementation to grow the coverage, I expect that will happen. But when (in corner cases) somebody improves old code, that is acceptable too –even when that comes with new “not TDD” code.
It’s unlikely that somebody, or a team, does –and stupid. But by defining the requirement in this way, it’s a
lot easier to count.
I value measuring progress over a strict definition.
Measure & show¶
Just measuring does not promote improvement; Is ‘42’ good?
It’s only a usable measurement when it was ‘41’ last week, and we strive to 43 next week.
Thus We need to show improvement. And so, we need to measure, store all values and display them in a graph. How? That
isn’t important. Possibly you can reuse the tools to show the sprint-burndown.
I usually start with a big sheet of flip-over paper; it costs nothing and only 5 minutes each sprint to update it. Remember: be(coming) lean is our chapter.
Count, count & graph!¶
Many definitions (and tools) exist to count coverage. For a start, the exact measurement is not that
important. For now, a simple, quick (and simple) visualisation is more important than anything else.
With a good toolsmith in the team, I would go for something simple as ‘grep & count’ functions in test and production code and show the quotient (over time). Someday, that number should be (far) above 1.0. Probably, you will start close to 0.0.
That number should be calculated per file (roughly unit-level), per directory (module level), and aggregated to system
level. Where one preferable should use requirement coverage, not code coverage. But again, keep it simple, give insight,
and don’t fall into the pitfall of theoretical wisdom.
When available, add a graph showing the ratio number-of-acceptance-tests to the number-of-requirements, again over time. Add it, don’t replace it –trust your developers. And coach them to use the right graph.
Because it is simple, developers can influence it and are motivated to add more tests. And to write testable,
That is the goal, so even when the counting tool is only an approximation, it will do!
Clean code is short-code¶
New functions should be implemented in a “clean, testable & maintainable” style code. Which suggests small functions.
Therefore I would also like to add a simple measurement as the “line-count pro function” visualised in buckets. For example, the percentage of functions that are less (or equal to) 5 lines, 10, 24, 63 lines, etc. Over time the smaller buckets should be dominant.
Focus on new & updated functions¶
Introducing TDD in an existing project is never perfect. Temporally, one should accept that existing/old code will have no or limited test coverage. Some ancient-styled, never-updated code will effectively never becomes better – on the other hand, when there is no need to update it, and it is field proven correct, there is no business value in making it better.
That does not apply to new units, they do not have a track record of correctness, and there is no reason to not write that code in a clean, testable way. And so, the team should be motivated to embrace TDD there.
This also applies to existing code that needs to be updated.
As it is changing the old code, the rule “don’t fix when it ain’t broken” is invalid; there is a risk of mistakes. Testing (and fixing bugs) is essential anyhow – even when that involves (manual) testing at the system level. So: apply TTD (and BDD) to that part, as it will be tested touchily it doesn’t add risk.
A pragmatic approach is to minimise the interface between the old and new code: don’t add many lines to an existing function. Instead, write some (small, clean, testable) new functions (with TDD), and add only a few lines to call them in the existing code.
That also prevents combining code styles in one file.
Where to start?¶
Many traditional embedded system organizations are a bit conservative to take advantage of modern software engineering principles. This is valid for Behavior & Test Driven Development too. It sometimes appears that “starting with” results in “waiting on”. Waiting on approval, waiting on tools, or maybe just waiting on a bit of help on where to start.
B&TDD is not a big bang!
There is no need to stop using the existing, good practices and replace them with revolutionary new, better ways. There are always places that are (too) hard to start and places that welcome the evolution of B&TDD.
Let me unveil some of those places. Places, as in location in the codebase, people in the organisation, or …
Or better, let me show you how to spot them yourself.
Developer versus Team¶
Although strongly related, BDD and TDD act on different levels. TDD is typically at the bottom of the ’V’; BDD is more
at the system (or acceptance) level.
However, that is often confusing for new adopters.
Therefore I often use a more pragmatic distinguishment: Individual Developer versus (scrum)Team.
A single developer can act following TDD. (S)he writes code, tests, and production code and switches between them every minute. As TDD is more productive, hardly anyone will notice it when somebody “secretly” adopts TDD. No extra tools or frameworks are essential.
That is hardly possible with BDD, as this is at the team level. A developer can’t run an acceptance test without the
assistance of a tester designer.
Despite this, a single team can embrace BDD – even when others don’t
As described above, new code (modules, classes, file) are to preferred above the existing ones. And in general, young
“modern” engineers are more likely to accept new ways than experienced “old” developers.
Try to combine that: Shepard fresh engineers to write small, relatively easy, and isolated pieces of new code and allow them to use TDD. Facilitate in a pragmatic undertaken – no fancy tools, just a few extra “test functions in the same language” using the same compiler, build files, etc.
In this way, one –almost secretly– make a start. Should it fail, bury it. When it works, keep it. One day, you can claim:
“TDD? Yeah, we do that for some time”!
The same applies to BDD: Only a single team is needed!
Again, I would vote for a new, (almost) independent module to be developed by a team of fresh, modern engineers.
Sometimes, the tradition of quality (assurance) can assist us to introduce BDD. When (automated) acceptance tests are
available, there is a great starting point. We only have to incorporate them in the ‘nightly build’ (aka the CI/CD
pipeline) – sometimes I use the excuse of “a baseline of regression test”.
Then, extend that set with new tests. And “grant” the team to run those tests before the developers with the code.
Again, sometimes it fails. But that is part of developing, isn’t it? We are used to fixing that. But sometimes, it works. One day everybody is busy, and the next day all tests pass. Then you report:
Yeah, we are done; we use BDD, and all our tests pass.
Really, we can ship!
Should I start?¶
The last question of today is more fundamental: ‘Should I start’? Today that is still an option. But will it be in the future? How long do you have the freedom to choose?
Albeit applying B&TDD in Modern Embedded System Software –especially with huge, aged codebases– is not trivial, using
Test Driven Development speeds up your team – some claim even 30%. And it results in better code with lower maintenance
costs. Likewise, Behavior Driven Development drives your team to focus on the right features, cutting costs by never
writing code based on the wrong requirements. And again, the system becomes better: less bugs.
When that is valid, it’s also compelling for your rivals. When they become 50% cheaper and 50% better, you don’t have many alternatives, then to follow.
IMHO, B&TDD is comparable with, for example, Object Oriented. Once, OO didn’t exist. Then, “desktop software” used it,
but we, the real-time-embedded community, continued to live in an assembly and C environment for some time.
Nowadays, even for embedded software, assembly writing projects are gone, nobody knows the Ada language anymore, and C is almost history. C++ is the norm in traditional embedded software, and some modern embedded systems are already switching languages, such as Python. Remember, even the Linux kernel is embracing Rust!
Our (modern) embedded software systems are changing the world. Probably it’s time that we change too. We have a
tradition of high quality, and we have demands to shorten the Time-2-Market.
When B&TDD can provide that, we should leave the famous waterfall behind!
This article on LinkedIn: https://www.linkedin.com/pulse/applying-bdd-tdd-legacy-albert-mietus