Predicate: I’m brand new to BDD / TDD, but I’ve done my homework.
I’m trying to put everything I’ve read / learned into practice with VS2010, SpecFlow and NUnit. Things are working, but it’s quickly becoming a chicken / egg scenario within a blur of unit and acceptance tests. I hope solving these BASIC questions will make everything click.
From my reading, I expected to:
- Write a Feature
- Define Acceptance Tests that fail
- Define Unit Tests that fail
- Make Unit Tests pass
- Now the Acceptance Test pass
- Write the next Feature
That all makes perfect sense, I’m just having a hard time seeing what’s what in code…
In practice:
- Write a Feature with Scenarios – fine.
- Generating Step Definitions – fine.
- Ahhh.
Does completing the step definitions (providing context in GIVEN, setting things up in WHEN and making assertions in THEN) constitute a completed acceptance test? Do you write out classes and methods here first as if you’d defined them already, and generate skeleton code via the IDE? Presumably the assertion needs some kind of implementation to work, but then the implementation code won’t have been unit tested? Ahhh.
I’ve looked at a number of example projects and codebases, but as they’re all complete there’s little sense of ordering in terms of the development process.
Any clarification of steps, examples, etc. would be great!
3
Your order is wrong, methinks:
- Define Acceptance Tests that fail
- Define Unit Tests that fail
- Make Unit Tests pass by writing the logic required for the feature
- Make the Acceptance Test pass by integrating the logic into the existing system
- Repeat with the next defined requirement
Basically, in both TDD and BDD, you always define what your new code should do before you write a line of that code. So, breaking it down, the real process is:
- Read the requirements for the next story, and break it down into individual pieces that can be defined in SpecFlow as assertions using “Given”, “When” and “Then” clauses.
- Write the first assertion into SpecFlow. This is your “acceptance test” and it will fail because the behavior is not in the system.
- “Drill down”: Identify the code object that is most directly interacted with from outside the system’s boundaries, which must exhibit the newly-defined behavior. That object does not necessarily exist in the current codebase. Write a test that asserts the object exhibits the desired behavior. Repeat; find the object(s) that the one you just tested must interact with to exhibit the defined behavior. Define tests that assert these objects do what they need to. Repeat until you identify one or more lines of actual code within a method which do not yet exist or which must change in order to begin exhibiting the behavior. Along the way, tests that must touch another object, the network, file system, DB, etc to have value are “integration tests”; tests that can operate within the boundary of a single class, or that have value given “test data” in the form of mocked objects, are “unit tests”.
- Now, the coding of the actual feature begins. Do what you have to to make the tests you wrote, and all other tests in the system, pass; unit first, then integration, then acceptance. It is not unheard of to write a code that passes first try; that indicates either a bad thing (you didn’t make the correct assertions; go back to step 3) or good (the behavior you need already exists; move on). It’s also not unheard of to make a change that passes a new test but fails an old one; either you made an incorrect change, or the assertions of the old test are now incorrect in light of the new requirement. It’s your job to determine which.
- Once everything is “green”, refactor the code you wrote to pass the tests; organize it, merge code blocks that do the same thing, adhere to good coding patterns. Make sure all the tests still pass, of course.
- Repeat from step 2 with the next assertion of the story requirements.
- Repeat with the next story.
Along the way, it’s perfectly normal to have to define a new object or object member that never existed before, but which must now exist and function correctly. Here’s where TDD starts paying you back, and where refactoring assistance like ReSharper pay for themselves several times over; simply code the test as if the code object you need already existed. This code obviously won’t compile; that’s your first goal, and ReSharper can fix pretty much all the red you’ll see with a few presses of Alt+Enter. Once it compiles, the test still fails because the skeleton exists, but the logic still doesn’t. Code that logic until the test passes, and then move on.
1
Generally, when doing BDD you dive in and out of a TDD stance. Your acceptance test defines the feature scope. Within the step definition, you should be ‘driving’ your scenarios. This is closer to what the user does, so maybe you use a GUI driver like Selenium. At this level you’re thinking in terms of your story role (as a…) and the actions available in the defined step context. Maybe this is a very high-level api, maybe it is the cross-functional flow of a process. The main attribute is that this is at the level of value to the role and should look like a user/consumer taking action.
When you define the high-level surface of your implementation, you then dive into TDD mode. Commit a TDD iteration, then surface back to BDD mode.
Imagine a scenario for a website:
- Given a Visitor
- And the Visitor is on the Registration View
- And the Visitor has provided all the required inputs
- When the Visitor Registers
- Then a Registered User is created
- And the Registered User matches the Visitor input
- And a Confirmation Email is sent to the Visitor provided email address
So, the step definitions all need to be filled in somehow. This is the BDD implementation mode.
Given a Visitor? Ok, what does it mean to visit the site? HTTP Request? How do I know? Cookies?
On the Registration View? Should I use Watin to drive this process. Are there URI end-points? How do I drive my application?
Required Inputs? How do I programmatically provide my system with user input? HTML Form?
Visitor Registers? How is this action triggered? Is this an HTTP Post? At this point, you’re seeing the top-level transaction-script-like view of your system.
At each point, you may flesh out your design using TDD.
Given a Visitor? Ok, new design choices. Is a Visitor a type in my system? Or do I call them a User with Registered=false? Use TDD to drive the design of a Visitor. Think about handling authentication in this level.
Visitor Registers? Is this a service? I want to say something like Register(visitor) in my test and drive it out using TDD.
Email is sent? Again, there’s a low-level and a high-level design to flesh out.
In sum, BDD scenarios (collections of Step Definitions) are statements about the worlds. Preconditions, Action, Postconditions (just like Arrange, Act, Assert) that are important to the user of the system. TDD is important to the programmer/designer of the system. But you use both when practicing BDD.
- Talk through the scenarios in conversation with your business expert or proxy.
- Question the scenarios. Find more scenarios.
- Write the scenarios down. Honestly, it doesn’t have to be in SpecFlow to start with.
- If it’s a new UI, it’s probably going to be wrong the first time, so if you start writing automated scenarios here you may find yourself thrashing horribly with them. Don’t write automated scenarios for new UIs. Wait till it’s stabilized. Get something small working – it can have hard-coded data if you like. Unit testing is fine here. Remember to get feedback from your business expert.
- Now that the UI has stabilized, you can automate the scenarios.
- Now the UI has stabilized, add another scenario that captures the next piece of behavior and make it work.
That’s pretty much how my cycle goes. It’s perfectly OK not to write automated tests for a brand new UI, and to write them afterwards.
Just make sure that either your business have clearly articulated what they want, or you’re both aware that they’re uncertain and you’ve worked out a mechanism for giving them something to try out so they can provide feedback. In that second situation you might not be able to come up with particularly coherent scenarios anyway.
2