Earlier this week I attended a three day Advanced TDD workshop delivered by Uncle Bob Martin. The course explained the principles and practices of Test-driven development as described in his book, Agile Software Development, Principles, Patterns, and Practices. It was an amazing experience, packed with great knowledge, so I couldn’t resist sharing my notes.
Uncle Bob (Robert C. Martin):
He has been a programmer since 1970. He is the Master Craftsman at 8th Light inc, an acclaimed speaker at conferences worldwide, and the author of many books including: The Clean Coder, Clean Code, Agile Software Development: Principles, Patterns, and Practices, and UML for Java Programmers.
– Skills Matter
We started introducing ourselves to one another. Everyone had to share when they started coding, when they started practicing TDD and what programming languages do they had learned so far. There was quite interesting distribution of coding experience with big group having 5-9 years experience and another one inside 25-29 years range. Almost half of the group had less than 1 year of TDD experience. The most popular language across attendees was Java.
All that time Uncle Bob talked about history of programming languages, which gave very interesting context to the course. Personally I found this particular quote very funny:
PHP – accidental language.
We learned about The Three Laws of TDD:
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
- GUI development – eyes should play a role of test runner.
- Your goal is to limit the time you spend debugging to the absolute minimum.
- When you write unit tests, you are at the same time writing documents presenting how the system works.
- Writing the tests after the fact is counter-productive and also in some cases very hard to accomplish.
Existing project example
Next saw an example of the FitNesse project which was implemented using the TDD approach. It contains ~70 000 lines of code where almost half of that is actually test code. It takes around 1 minute and 40 seconds to execute 2 000+ unit tests and 200+ acceptance tests. If the build finishes successfully it means we can deploy changes.
We saw a coding example of how the aforementioned laws of TDD should be applied. We observed how a stack can be implemented in Java from scratch using the TDD approach.
- People don’t always like to make their test fail before it passes, but it doesn’t
take very much time and helps you keep in the loop. It’s a skill that evolves with time to being a non-tedious part of a workflow.
- You are not allowed to refactor until all your tests pass.
- Avoid reaching the goal discipline – write minimum code to make the tests pass. You don’t need to think deeply, you only add complexity one step at the time.
- Uncle Bob almost never uses comments in assertion statements.
- Principles and laws are great when you start with TDD. When you master the TDD cycle you can treat them as guidance and cheat from time to time.
- Refactoring is a change that never breaks tests. It produces cleaner code.
I paired with Mohammed to implement from scratch queue. We did it in Java using the TDD approach presented in the previous session. We did our best to follow the rules we talked about in the previous session.
We started with a kata for the Prime Factors algorithm.
We could observe that when tests get more specific, the production code gets more general.
We also did a sort of experiment and went through the thought process of how would we implement a sort algorithm. We quickly realized we were creating a bubble sort algorithm, which resulted in the joke:
Maybe TDD is a great way to produce bad algorithms? 😉
- TDD is a way to derive algorithms.
- If you are looking for some ideas for how to make your code more general you can search for this keyword, transformation priority premise or check this blog post.
- If there are two implementation paths, you can commit your code and try one of the branches. If it fails you can always come back to the last commit and try another one.
- There is never a task in a plan that is called refactoring, you always do it in the TDD cycle.
We did another coding exercise. This time we had to solve a kata for the Word Wrap riddle. We were left with a hint, if you get stuck and need to rewrite all code it means you have written your tests in the wrong order.
I paired with Mohammed again. We learned ourselves doing this kata that you should always select your next test case with great care. It should be only a bit more complicated than the previous one. If you go quickly for the most complex use case you most likely will fail.
We solved a kata for the Bowling Game problem as a group.
We have started with an upfront design which led us to draw UML diagram with four classes.
We were doing pair programming where Bob was the driver and everybody else played the observer role. At a very early stage we broke one of design principles, we misplaced responsibility in one of the methods which we quickly noticed thanks to the way we reason when using TDD approach.
We ended up with only one class instead of four. We implemented only two methods which had actual logic only in one of them. It was as simple as a for loop with two if statements.
- It’s okay to refactor a design, but if you need to change the algorithm, it means you did something wrong.
- When you do a pair programming you can easily miss something, but your pair partner can help you spot errors much faster. Bob made a tiny mistake in the code during refactoring which we spotted. He said it happens almost all the time because of the stress factor.
- In production code you should put private methods at the bottom of the class. However in your tests you can put private utility methods just after the setup section to keep all non-test related logic together. The idea here is to keep utilities grouped together.
- Pair programming is exhausting. The maximum should be set at 2-3 hours a day and it’s not necessary on simple tasks.
We moved to the topic of testing code with dependencies. You want to look first the following before you go mocking:
- Test Specific Subclass – you take an existing legacy class and override method that is expensive to call.
- Self Shunt – a variation of the former. The test derives from the class under the test and overrides the methods you don’t want to call. It’s even easier to do verifications.
The name comes from Hollywood stunt doubles. We can distinguish:
- Dummy – it implements an interface and every method is meant to do nothing. It’s useful when you need to pass expensive object which you don’t execute in the actual test.
- Stub – is a dummy where a method returns the test specific value. When you have expensive object you can stub the method and enforce state you need to have in your test.
- Spy – is a dummy which remembers things. They can become very complicated and basically record everything.
- Mock – the real mock. It is a spy, a stub and a dummy at the same time. In addition it also can verify if everything that was expected happened.
There is also a Fake. It’s an object that emulates business rules. The problem with them is that they tend to grow and get very complicated.
Most of the time it’s extremely trivial to write your own test double. Bob creates in most cases stubs and spies, which is his default. There are times when mocking tools are very useful and you can benefit from their super powers. Those cases could be having complicated setup or the need to test very complicated legacy code.
Third and fourth sessions
We extract the logic into a separate easy-to-test component that is decoupled from its environment.
Testing the GUI or hardware is hard. Therefore you should leave those parts humble. It means they should contain as little logic as possible, so you could feel confident enough to leave them without the tests. That way you can focus on testing all the parts that require computation.
In the view layer example, you would create a presenter which provides all values to the UI which then are displayed as they were passed. Remember that unit tests can’t test everything.
Environment controller exercise
We did another coding exercise to stress out what we learned so far.
We had to implement an environment controller which controls a real HVAC (heating, ventilation, and air conditioning) device. The controller had to regulate room temperature using heater, cooler and fan. Constraints:
- When temperature is 70 ºF, everything is off.
- When temperature is over 73 ºF, cooler and fan on should be on.
- When temperature is below 67 ºF, heater and fan should be on.
- Leave fan on for 5 minutes after turning heat off.
- Don’t start cooler within 3 minutes after turning it off.
This time I paired with Tim and we decided to do C# implementation obviously using TDD approach. We were told that we most likely are going to need the humble object to make sure an actual device API gets called. We were adding tests and their implementation in the following order:
- Given that actual temperature is 70 ºF, then everything is off.
- Given that actual temperature is 74 ºF, then cooler and fan are on.
- Given that actual temperature is 66 ºF, then heater and fan are on.
- Given that actual temperature is 74 ºF, when it changes to 73 ºF, then everything turns off.
- Given that actual temperature is 66 ºF, when it changes to 67 ºF, then everything turns off (which seems wrong now…).
- Given that actual temperature is 74 ºF, when it changes to 73 ºF and back to 74 ºF, then cooler doesn’t start.
We didn’t manage to finish this exercise, though it seems we were very close. Anyway, I wanted to give you an idea what our thought process looked like.
You can also check one of the Uncle Bob’s implementations here. The most noticeable difference is that he used meaningful constants instead of temperature values, like too hot instead of 74 ºF.
- Fast – test runs < 1 second. If test suite takes too long, split into bunch of packages which run much faster. If it takes too long you want run them often enough. To speed up your tests you can mock up your database. Frameworks are perfectly fine, but they should be kept out of your business rules.
- Isolated – one test shouldn’t depend on another. Some test runners shuffle tests order to mitigate that issue!
- Repeatable – your tests should pass in all environments and on all machines. Don’t depend on network connection or framework. Frameworks are out of your control, and hence should be considered as dangerous.
- Self-verifying – pass or fail, no processing.
- Timely – test first approach, one test at the time.
Bdd vs Tdd
BDD and TDD are very similar under the hood. BDD introduces Given When Then terms, which with some discipline can be translated to the TDD vocabulary Arrange Act Assert.
There should be no reason to test private methods. Sometimes testing is more important than encapsulation. If you are testing private method it gives you feedback that probably your design is broken. In that case you can consider the extraction of the private method inside its own class.
- Our goal is always to get to 100%, which is generally impossible.
- Test coverage should be visible to everyone.
- It shouldn’t be mandated by managers.
First we learned about a couple of test metrics and then we had questions and answers panel.
Whether test behavior or test state?
The statists don’t like mock testing, because that couples your implementation with tests. There is no real answer. Across architectural boundaries mocks are way to go. Inside business logic state testing would be much better choice.
What should you do when your function grows too big?
In many cases very large function gives you a hint that it should be really extracted into its own class.
What if you do change in one place and it breaks other parts of the application?
It means you have bad design. If you change one place and other tests fail, it also means there are issues with your architecture.
Do you thinks it’s a good idea to keep all your test code inside one test method?
No, it’s always great idea to abstract your production code inside tests. That way whenever production code changes you limit number of places you need to change. It also helps you focus on the actual test when reading code.
FitNesse – it’s a wiki and stand-alone acceptance testing framework. Wrapper to Cunningham’s framework for integrated test (Fit) project. It allows to write acceptance tests for non-technical people.
In all acceptance tests we should try to avoid using user interface.
Manual testing is horrifically expensive. Unit tests are formal specification of code. Tools like FitNesse or Cucumber are meant to bridge business and technical divisions. Acceptance tests should be always written upfront. Use only those tools when business can be involved, otherwise you can stick with the regular testing frameworks.
QAs are more valuable if they do their work upfront specifying all the requirements. We as a developers should guarantee that system works as expected. The developers should do the sign-off.
- You can simulate random number generation by using statistical distribution.
- 1 week sprint works the best. Initially agile sprints were meant to be 4 weeks long.
Testing the user interface
Testing the user interface violates the fundamental rule. We are coupling our tests to UI which is supposed to change in a flash. You should test business rules by bypassing UI. You should test business rules by exposing them to tests using API. You have to test UI, too. You can test that independently You can do that by plugging in dummy business rules while testing UI.
Different types of testing
- Unit tests (coverage nearly 100%, done by developers) – part of CI.
- Acceptance tests (~50%, business analysts & QA) – part of CI.
- Integration tests (~20%, architects) – they should care only about a couple of layers at the time, executed once a night in dev environment.
- System tests (~5%, Business analysts & Architects) – run in staging, at that level you can test 3rd parties.
- Manual tests (exploratory, people) – you don’t follow a handbook.
Our industry is the last one that needs to understand that testing is important.
We listened about history of object oriented programming. The concept of polymorphism changed everything completely. It allows you to define boundaries. More volatile modules have to depend on the less volatile modules. That way you can easily test your module within boundaries.
Our software should scream about what our application does. Very similar talk that covers fully this topic can be found on YouTube:
Systems like Git allow us to travel in time. At some point database could be replaced with systems that don’t do updates or deletes. They would store only single transactions.
Never let your tests to run production data. Always insert data for your tests and clean them up afterwards.
The purpose of architecture is to delay big decisions. A good architecture maximizes the number of decisions not made.
We did another session of questions and answers panel.
How fast inexperienced developers should start doing TDD?
Even if they don’t understand the idea they should start doing it as soon as possible. They can use checklist which is going to guide them until they master the skill.