I do frontend AngularJS work for a client in Portland. One of the things I really like about their setup is its test-friendliness. They use Grunt to watch the client-side files and run tests and re-compile everything automatically when a change is detected. Everything gets wiped from the “build” directory when the process starts, and a failing test or JSLint warning blocks the whole process. This renders the developer’s copy of the web app inaccessible until the issue is addressed. I’ve found I enjoy this particular workflow as I’m immediately made aware of when I write something that breaks the tests. It keeps me from building on top of broken foundations.
This weekend I decided I wanted to sharpen my C skills by writing a simple CLI utility to convert strings of hexadecimal to memorable phrases and back again. Since my intended use case was encoding and recovering 256-bit private keys, I wanted to take extra care to ensure correctness of output. So I set out with the intention of writing both unit and user acceptance tests using a TDD flow similar to the one I use in my AngularJS work.
One of the things I really like about C is its tininess. In light of this, and since I was just writing a small CLI utility to push bits around, I sought out the most minimal solution I could find. I decided to use MinUnit, which I modified a very little bit to more resemble KarmaJS‘s syntax:
This allowed me to write my test runner like this:
In all, I ended up writing 33 such tests. Running them was just a matter of compiling my
tests.c file and then executing the resulting binary. I put together the beginnings of a Makefile to help with compilation:
The last piece for me was getting something in place to watch the file system and then kick off
make tests and run the resulting binary. inotify-tools supplies a CLI utility called
inotifywait that allows one to listen for filesystem events and blocks until a filesystem event is seen. As luck would have it, I was able to install it under Ubuntu with
sudo apt-get install inotify-tools. I then wrote a script to take advantage of it:
The above script is pretty simple. It just defines a function called
run_tests that silently (
-s) runs the
tests tasks in the Makefile I wrote, and then runs the
unit_tests binary produced. If I pass
-w as the first argument to the script, it clears the screen, runs the tests, and then waits until one of the
.h files in the project’s root is closed after being written to, and then repeats the whole process until the script is halted. If
inotifywait is not on the developer’s system, or if one is using something like Travis-CI to run tests, executing the script with no flags just runs the tests and exits.
Now that I had a solid workflow for my unit tests in place, it was time to put together my user acceptance tests. This proved to be pretty simple as well.
User Acceptance Tests
Since user acceptance tests are all about testing the whole system, first I updated my Makefile to allow building the project within a subdirectory:
Once I’d done that, I just put together a shell script to compile the utility into a subdirectory and check its output against what was expected.
I ended up writing four more tests using the same format as the example test shown above. With inotify-tools installed and the combination of C code and bash scripts outlined above, I had a pretty nice TDD workflow set up. The only issue was that sometimes
unit_tests.sh blocked each other by trying to access the same files at the same time. To resolve that issue, I wrote a third script to touch them off in serial:
With that, I had a reliable testing workflow that kept me constantly updated on whether my C code was passing its tests or not. It felt a lot like my AngularJS workflow, and it helped me catch a lot of bugs the moment I wrote them rather than further down the line.
Later on, I also added desktop notifications to show me my test output using notify-send, a CLI utility that comes installed with Ubuntu. You can take a look at the later copies of
unit_tests.sh on Github if you want to see how I did that.
Any questions or suggestions about my TDD setup? Comments are always welcome!
Addendum: Apparently I could have done without maintaining a list of function calls to my tests. That would have streamlined my workflow even further!
Addendum 2: As someone on reddit pointed out to me, there are already a number of TDD frameworks for C, not the least of which are Check and TAP. If you’re planning on building anything bigger than what I’ve built here, you may want to use something more substantial than what I came up with.