Last week, TrackAbout pushed SpecEasy, its first open source project, up to GitHub. I thought I’d give a little history and introduction to the project.
For a good chunk of last year Jeff Sternal and I did remote pair programming, and we both wanted to practice strict TDD/BDD to further develop our thoughts on the practice. We’d both done enough to feel the benefits it gave us in forcing us to think about the design of the code. But we wanted a better feel for how, and if, it changed our speed and code quality.
Our biggest challenge was the large amount of friction that came when trying to follow Red/Green/Refactor. First, our builds were slow, so we worked to speed them up. After that, the in-house BDD framework we were using, which was almost identical to SpecsFor, was way too verbose. If you check out the first example on the SpecsFor home page you’ll see that it takes 40 lines to make three assertions, and if they wanted to add any more, it would be at least 6 additional lines per assertion. This is not the readability increasing whitespace you’re looking for.
In looking at other possibilities we found NSpec and admired the general approach, but we wanted to avoid using a new test runner. We also didn’t really click with the terminology used, because it felt like it made the tests harder to read, and therefore harder to understand.
We felt that if we could make the test/spec writing much smoother, the whole process of TDD/BDD would be more productive and start to give us the gains we hoped for. So over the course of a few sprints we developed SpecEasy1 and used it in developing our assigned backlogs with a BDD approach.
Our main goal in writing it was to reduce the friction. We wanted the tests be both terse and as readable as possible. We also wanted to eliminate or reduce the duplication that our SpecsFor approach led to. NSpec pointed the way to making things more terse, though we did change quite a few minor things (terminology, syntax) to make it read easier for Jeff and I. We also wanted it to work seamlessly with NUnit, since we already had thousands of NUnit tests in both our website and mobile app solutions. At the time, our code made heavy use of Ninject for resolving dependencies and newing up objects, so we also wanted a solution that (like our previous one) could use a RhinoMocks Ninject mock repository in the test code.
Two things came out of this work that we weren’t really shooting for but that are really nice. Both are side effects of building up tests using strings that describe what’s going on. One of our goals was to reduce how much typing our old SpecsFor-like BDD framework required. In SpecsFor, to reuse test setup required inheritance to build up contexts for tests, or just copying the setup code around. If we used inheritance we were required to repeat aspects of a context in the inheritance syntax, so either way, duplication of information in the tests. But by building up tests using the simple Given/When/Then of BDD, nested contexts, and strings to explain what was going on, we eliminated a lot of the duplication of test descriptions.
The first aspect of this is that by putting those strings at the front of each line of code, they become a syntax-highlighted description of the test code. Check out this picture of a simple set of tests for an on-screen keyboard:
By reading through the red strings, you can get a pretty good feel for what the test is checking, without trying to decipher the actual code performing the test. Because the tests are self-documenting, it’s easy to read the code and add new tests, which is a common thing to do in large codebases.
The second benefit is closely related, but comes in the authoring stage. When writing each statement of a test, we’d start by writing the description of what it would do. This let us think about the test in everyday language. But then we’d immediately translate that description into a closure that actually did the work. The flow that came from doing that worked surprisingly well. And while SpecsFor can be used in a similar way, the additional work required by its more verbose syntax eliminated that flow that we sought, which is so important for effective BDD/TDD.
Once we got things to a point that we really liked using it, we shared it with the TrackAbout team, and others on the team have since made further contributions, including its current name and a sweet logo (thanks, Mike!). We released it on GitHub last week as the first open source project TrackAbout has developed. In addition to sharing it with the world, part of the reason we’re making it public is because there are further changes we’d like to see happen. SpecEasy is far from perfect.
The use of closures to capture variables that are then used while the tests are run leads to some weird quirks. You need to declare variables in your method itself (not within the anonymous delegates that get assembled into actual tests), but you need to initialize the variables in those anonymous delegates, so they get reset when running through different assertions. I’m not really sure how we can improve this aspect of the experience, but it’s something I’d like to spend more time thinking on, and get ideas from the community.
Because we’re using nested Given calls to build up contexts for running tests, and asserting different things, different (but related) contexts have been built up, it can occasionally be hard to get the right parentheses and bracket nesting. Mike Mertsock recently made a great change to reduce the nesting in one way, but I believe there is more we could do to clean up the syntax, further reducing the friction of writing tests.
There are lots of smaller issues as well. Right now, SpecEasy uses Nunit, Ninject, and RhinoMocks. It would be nice to make the usage of those tools optional, so that you can use your favorite test runner and mocking framework. We should make a public NuGet package. We could use an NUnit extension to make each assertion into separate test. In practice, it would be nice to have an easier way to specify multiple Given clauses. And the list goes on.
Anyway, if TDD is your thing, try it out. And if you want to help us solve the remaining issues, just fork the code.
- SpecEasy was originally called NuSpec internally