Test driven development (TDD) doesn’t seem to be all too common among TwinCAT-developers, which is a shame. From my experience, TDD has a strong foothold everywhere among developers, but TwinCAT? Not so much. And I don’t blame them. There are TDD frameworks for C++, C#, Ada, Python and basically any other language and/or development environment. Do a Google search on the web on any programming language/IDE and TDD and you get thousands of results. Do the same for TwinCAT and you’re on your own.
This is mostly the reason I’m writing this series of posts. I’ve decided to write a total of seven posts about TDD, divided into:
- Explaining the benefits of TDD (this part)
- Setting a baseline for an example of a TwinCAT library that we will develop as an example for unit testing. Here we’ll create the header files for the functionality that our library must provide
- Creating the (failing) unit tests for step (2) above
- Same as above (second part)
- Do the actual implementation (body) of step (2) until our unit tests in step 3/4 succeed
- Same as above (second part)
- Try out the developed software on actual hardware
My goal with this series is to at least convince one TwinCAT developer to try TDD on his/hers next project. I’m going to publish one new part of this series every week, so part two of this series is posted next Thursday.
TDD is the practice of writing a test for some required functionality, before writing any implementation code. The idea is that when you run your tests the first time, they will fail. After you’ve written your (failing) tests, you do the actual implementation, until the tests succeed. Holding to the TDD discipline means that you don’t fall to the temptation to write tests after the implementation, but before. In this way, you do your code in iterations as quick as possible, until the code under test passes the tests. After passing the test, you refactor your code until it holds up to acceptable standards. When you’ve done this for some time, you naturally write function blocks that are quite small, and which conform to the SOLID principles of object oriented programming. When doing software development, what usually happens when the code base grows is that it gets harder to change the code base as the developer gets scared that some existing functionality might get broken. One of the properties that these tests have is that you suddenly have a regression test-suite of your code. If you find it necessary to do any changes to the software (for instance, by refactoring or if you want to add functionality), you have all the tests which you easily can re-run. This increases the programmer’s confidence in making larger architectural changes when adding new functionality. Once you’ve written tests for a time, it gets natural to test small sets of functionalities at a time, and thus your function blocks usually end up quite small. TDD thus leads to more modularized, extensible, and flexible code. When defining the tests, you’ll automatically define what the function blocks under tests should provide, and thus you’ll end up with clear defined interfaces for the function blocks. The unit tests shouldn’t just be software for validating the application, but is equally much a mental engine for the design of the software.
As you’re writing test cases, you get better test code coverage and thus fewer bugs. As your test cases are written, you come up with scenarios and run your code that you normally wouldn’t run under normal test circumstances. Any developer that is going to look at your code will get documentation of what the code is supposed to do looking at the test cases. The test cases dictate what outputs every function block should provide given a set of inputs. With this information, any developer that looks at the test cases gets a better understanding of what the function block is supposed to do, so the test cases become examples of what the code should do. When developing a certain set of functionalities, you need to define what that functionality is supposed to provide. With unit test cases, this is exactly what we are doing and thus these become acceptance criteria. As every test case that you are writing is for a single piece of functionality, you will have to think what interfaces that functionality needs to provide for the rest of the code to be integrated. As the test cases are written first, only these interfaces needs to be initially defined. All the other methods and inner workings of the implementing code can be made private! This will result in tidier code.
Once the tests are in place, and you write your code until the code passes the tests, you’ll sense a feel of accomplishment knowing your code passes the tests. When you work in a larger project and have thousands of tests that are to be executed, it’s a nice feel to have all the tests pass OK! Starting with TDD in the beginning will feel like you must double the effort, as you now not only need to write the code itself but also the test cases for them. First off, what you’re doing is saving time that you will later have to spend fixing all the bugs. Second, when you’ve done this for a while it doesn’t take so much longer to write some test cases before starting with the implementation. And on top of that, you get all the other benefits which I mentioned earlier. It’s the starting ramp-up time to get into the “TDD thinking” that takes the longest. Writing tests costs time, but overall development takes less time.
Now you might think “Ok, I get it! TDD is interesting, but what about practice… there isn’t anything mentioned about TDD in the TwinCAT documentation! How do I start if I want to do unit tests for my TwinCAT code?”. There are testing frameworks for other languages and development environments (as JUnit for Java, NUnit for C#, Check for C) that provides a full testing framework for the developer. This does (TwinCAT 3.1.4022.2) not yet exist for TwinCAT (Update 2021-04-23: now there is a unit testing framework for TwinCAT)
and most likely will not be available for quite some time. But to create unit tests doesn’t require a unit testing framework. Unit testing frameworks give you a lot for free, but they are not a hard requirement for writing and running unit tests! All of this will get clearer as we continue to the two next parts of this series.
In the next part of this series, we are going to develop a library for IO-Link communication, more specifically handling the reading of IO-Link events using TDD methodology. Stay tuned!