TLDR - Rake Tasks can be broken into objects, objects can be easily TDD’d.
Sometimes I forget that my best work occurs when I forego the cowboy coding, or unplanned unit of work because I already know how I want to implement something. That stupid lesson I keep having to learn over and over again is about testing first, driving design from need instead of gut feeling.
Rakefiles tend to give me a junk drawer of untested code I use consistantly but without tests. (Thanks to @sarahmei for the perfect wording here)
The thing is that no matter what I allow my gut to feel, or my instinct to drive, I have to abide by processes. Processes provide repeatability, both in results and in how I accomplish those results. TDD provides an awesome process to drive a design. This doesn’t mean I should never deviate, but as Sandi Metz stated, follow the rules until you understand why you should not.
Rakefiles consists of Tasks and Rules, in this post I’ll address tasks. In a later followup, I’ll go into detail on testing rules.
So the thing that I was working on that brought this about was a single task. The task was to get a list of files that are different in my git repository from what is located in my work’s PVCS repository and insert it into a word document for our Configuration Management team. The task is repetitive and enough of a pain that once I was able to install Ruby on my work desktop, I immediately wanted to change it to a Rake task.
Before writing the tests I started with a spike (or for those who don’t know my code word, I did it without writing tests first).
Here was the task as it stood at first.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Yes, this functions well, and doesn’t insert anything into a word document. This is the point where I realized what I was doing was in desperate need of TDD and not cowboy coding. So let’s try this. My first (unsuccessful) attempt was to reuse the above code and just write a test. Without going into too much depth, it is very hard to setup a test repository with a checked out working copy and then running rake to verify that the rake task works.
My next approach was a somewhat more sane approach. It dawned on me that everything within the task do end block needed to be broken out into it’s own object. Objects are infinitely more testable than testing the rake task. My side note here is that it also dawned on me that testing the fact that the task runs is similar to testing rails generators. Basically, rake has a maintainer, and it isn’t me.
So breaking down the task contents into a TDD’d object is a little simpler.
The TDD process is fairly simple: Write a test. Then write just enough code to make the test pass. Afterward refactor the code. And finally refactor the test. Below is the resulting tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
And with these tests the new FileChanges class becomes a tested task I can add to the Rakefile.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
And the resulting rake task is much simpler, and now has tested code driving it.
1 2 3 4 5 6 7
This makes the task a single line and gives me test coverage for the task in the Rakefile.
Hopefully this illustrates how I test rake tasks. Trying to test the functionality of rake along with the task is hard and a good indicator that I was testing the wrong thing.
The tests guided a refactor from an outside in approach. I could have improved upon the tests by stubbing the diff method, but I thought it better to show my honest work.