(I originally posted this on my MSDN blog.)
As I wrote in my previous post, BDD is largely about preserving the flow of intent from your user stories to your unit tests (specifications, in BDD parlance) to your product code. As developers, we’re in the habit of switching over from user intent (features that solve problems) to developer intent (mechanics of the code) when we write tests, but preserving as much user intent as possible all the way through is a lot better for maintainability and it helps drive better initial designs, too. It’s the same as the ubiquitous language in DDD. Don’t abandon the ubiquitous language; stick with it as long as possible.
In other words, don’t focus on how the code works, think about how the system behaves. The code will follow naturally.
It turns out, though, that it’s surprisingly hard to do this well. At least, I find that I often have to remind myself to leave implementation language out of my context and specification names and to focus on how a user would describe the behavior in domain language.
I recently prepared a presentation on MSpec for my group at work and I used the Camera specs in the GenesisEngine project as an example of specs written in the behavior-focused style. There’s nothing like putting your work in front of other people to make you take a fresh look at it with a critical eye! As I read over my Camera specs, I realized that I had let some implementation language sneak in when I wasn’t looking. In other places I had been pretty vague about the behavior that was actually expected.
For instance, consider this context:
[Subject(typeof(Camera))] public class when_view_parameters_are_set_by_look_at : CameraContext { Because of = () => _camera.SetViewParameters(new DoubleVector3(0, 1, 1), DoubleVector3.Zero); It should_set_the_camera_location_correctly = () => _camera.Location.ShouldEqual(new DoubleVector3(0, 1, 1)); It should_set_the_camera_yaw_correctly = () => _camera.Yaw.ShouldEqual(0f); It should_set_the_camera_pitch_correctly = () => _camera.Pitch.ShouldEqual((float)(-Math.PI / 4)); It should_set_the_camera_roll_correctly = () => _camera.Roll.ShouldEqual(0f); It should_set_the_camera_view_transformation_correctly = () => _camera.OriginBasedViewTransformation.ShouldEqual( GenerateOriginBasedViewMatrix(_camera.Location, _camera.Yaw, _camera.Pitch, _camera.Roll)); }
It should set location/yaw/pitch/roll/transformation correctly? What the heck does that mean? That tells very little about what my intent actually was. I was just being lazy and didn’t want to bother with trying to carefully describe the intent.
Actually, I bet what I was thinking was something like, “Hmm, my expectation here is that these properties should be set to specifc numbers. I don’t want to state those numbers in the spec names, though, because that’s a test detail. I’ll just say it should set the properties ‘correctly’ because that sounds more generalized.”
But what was my real intent for the behavior of the camera when the view parameters are set via a look-at point? Well, the real intent is that the camera should go to the requested location and set its orientation to whatever values are needed to face toward the look-at point from that location, and finally generate a new view transformation based on the new camera state. Ok, now that’s a description that’s phrased in terms of the problem domain, not the implementation domain. Let’s see if we can improve those specs:
[Subject(typeof(Camera))] public class when_view_parameters_are_set_by_look_at : CameraContext { Because of = () => _camera.SetViewParameters(new DoubleVector3(0, 1, 1), DoubleVector3.Zero); It should_go_to_the_requested_location = () => _camera.Location.ShouldEqual(new DoubleVector3(0, 1, 1)); It should_set_the_yaw_to_face_toward_the_look_at_point = () => _camera.Yaw.ShouldEqual(0f); It should_set_the_pitch_to_face_toward_the_look_at_point = () => _camera.Pitch.ShouldEqual(-MathHelper.Pi / 4); It should_set_the_roll_to_face_toward_the_look_at_point = () => _camera.Roll.ShouldEqual(0f); It should_generate_a_view_transformation_for_the_current_state = () => _camera.OriginBasedViewTransformation.ShouldEqual( GenerateOriginBasedViewMatrix(_camera.Location, _camera.Yaw, _camera.Pitch, _camera.Roll)); }
That’s better.
In the location spec, I got away from the implementation language of “setting the location (property)” and used the domain language of “going to a location (in the world)”. Very similar, but different perspectives. For the orientation components, I described the intent of facing in a particular direction. And for the view transformation, I called out the fact that the transformation is dependent on the new state.
Now, a lot of you may be looking askance at me right now. Isn’t this nitpicking pretty silly? Well, sure, it’s not earthshattering or anything. I didn’t fix a bug or implement a new feature with these changes. But I think I made the code a little bit cleaner, and that’s the real reason why I started this project in the first place. It’s not about terrain rendering, it’s about fine-tuning my techniques. Lesson learned: avoid writing specs that say something should be done “correctly”. Describe what the correct behavior actually is.
The diff of the improvements I made to the Camera specs can be found in two parts here and here.