Test-Driven Development
Test-Driven Development is a technical practice that supports Agile's iterative and incremental development cycle. TDD helps you quickly discover mistakes, preventing defects. You weave a test safety net as you grow your product's behavior one test at a time. This safety net supports you now and in the future to help you keep code working as you change it. Oh yeah, don't let me forget to tell you it's fun and once you learn it, you save time and money.
Maybe you have heard of Test-Driven Development but don't quite get it. A good way to understand TDD is to pair program with an experienced practitioner. We will start with a brief overview and demo of Test-Driven Development. In this interactive workshop, you can practice TDD in C. You don't need to install any tools for this workshop. You'll run the exercise on my exercise server. You will know what TDD is after this session. We'll conclude the workshop with a debrief on your experience.
Before attending this workshop, it is highly recommended that you watch this talk from the 2020 Embedded Online Conference.
According to James Grenning, what is the primary practical benefit of practicing Test-Driven Development (TDD)?
It sounds like the thing left of an equal sign is not assignable, it is not a lvaue (L-value)
You will need to provide a snip of the code if you need more help.
For ex: ADC1_RB = (uint16_t)(10); Is accepted by one compiler but gives an error on a different compiler (different microcontroller). Could a compiler block assignment of a register in a simulated environment in your experience? I tried searching for this specifically on the web but couldnt find anything. even asked ChatGPT ;)
It looks like you are using a silicon vendor-specific compiler. ADC1_RB is a device, right?
The silicon provider may be trying to be helpful, but as they extend the C compiler, your code is now handcuffed to their silicon. That's not a good thing for product software to be so firmly connected to the silicon. You should strive for minimal hardware dependencies and portable code.
The solution is pretty easy, you can either wrap all HW interactions in simple functions or macros.
Using functions and linker substitution
During test, you can provide a link-time test-double for void adc1_rb_write(uint16_t value) for test, and link with this function for production.
void adc1_rb_write(uint16_t value)
{
ADC1_RB = value;
}
Using Macros and pre-processor substitution
You could use a header file that defines the hardware interactions. This is the one used in production. During test, you could make a test double structured similarly.
#define ADC1_RB_WRITE(value) ADC1_RB = (uint16_t)(value)
You would use two header files with the same name, and configure your build for production or test.
Perfect! Thats what I was looking for...I was thinking so much on it that I forgot that I can do what you just suggested!
Just finished all of the unit tests in the exercise.
The recommended API change to accommodate getting from an empty queue felt unsatisfying to me. I understand that it was done to avoid having to refactor almost all of the unit-tests. I think we were all aware (at the start of the exercise) that a "get" could fail. Should that API have been design differently from the start? i.e. bool CircularBuffer_Get(struct CircularBuffer *, int *);
Does this highlight a weakness in the TDD methodology?
Hi Barrie
Good question. There were two API changes, and both were painful.
CircularBuffer_Create() changes to
CircularBuffer_Create(size_t capacity) changes to
CircularBuffer_Create(2)
In my live training course, we spend time on this that we could not in the workshop. For starters, in my examples, I set up problems that we can later discuss. I want people doing the exercise to feel the pain. With that, they can better appreciate the solution. API changes are painful, and somewhat unavoidable over time. So, it is important to have a way to deal with them.
BTW: I have enabled more of the surrounding material. You can watch the "requirements" discussion where the API is identified. I have also enabled full demos of me doing the solution. You may want to check them out.
There are two takeaways from the live course about API evolution...
1) Try to get an API right, from the beginning. -- BTW: this is not always possible, as things change in unanticipated ways. In this example, we could have gotten CircularBuffer_Create(size_t capacity, int default_value) right. We anticipated this interface in the video discussion.
2) How should we make an API change and not break all the clients at once? There is a mechanical refactoring approach that helps keep the code working while we change it. I would have said this in a live training class numerous times: "It's easier to keep a system working than to fix it after you break it.". Here are the steps to add the capacity parameter to CircularBuffer_Create(void) and not break existing code and tests.
- Test drive the new API function to have the needed behavior while using a temporary name like
CircularBuffer_Create2. - One at a time, replace calls to
CircularBuffer_CreatewithCircularBuffer_Create2adding the neededcapacityparameter. - Once all the
CircularBuffer_Createcalls are gone, deleteCircularBuffer_Create - Rename
CircularBuffer_Create2toCircularBuffer_Create
All existing tests passed through that change.
Regarding your proposed CircularBuffer_Get() API, I prefer the programmable default return value over a modifiable parameter. I know modifiable parameters are very common in C programs, but I try to avoid them.
Thanks you so much for your detailed reply. I will look into seeing if our organization would want to benefit from your more in-depth training course.









Any way to avoid this error during register assignments? I use Unity framework.
Error[Pe137]: expression must be a modifiable lvalue