Test Driven Development Benefits & Techniques
- Posted by sumerge
- On October 3, 2017
Test-Driven Development (TDD) is a prerequisite for professional behavior: think about software as a sensitive discipline. I mean that there are tiny places of data that could crash the system if set the wrong way. Like software, the accounting is sensitive as just a single digit may cause a huge effect and send executives to jail. So accountants deal with this sensitivity of their work by applying techniques like double entry bookkeeping; TDD is double entry bookkeeping.
Benefits of TDD
1. Eliminate the fear of changing code to better design or structure
As the code is covered by unit tests that guarantee that it’s fully functioning, I can easily refactor the code and run the tests in every step in my refactoring to make sure it’s still working.So we can’t write a clean code unless we eliminate the fear of change.
2. Reduce debug time
Debugging skills shouldn’t be desired. Instead of spending time debugging, we should spend time writing a code that works.
3. Complete and reliable low level documentation
By applying the three rules (as we will discuss later in the post) of Test Driven Development, we get a design document that describes the low level specification of the application.
4. Improve design
Writing test first makes your production code testable (accessible from tests), as the test will have a tremendous influence on the production code which is to make the production code testable. Another word for testable is decoupled.
Three Rules of TDD
TDD is a discipline and as a discipline it has a set of rules. Think of this discipline as a doctor washing his hands before an operation:
1st law: You are not allowed to write any production code unless you write a failing unit test.
2nd law: Write enough of a test to demonstrate a failure.
3rd law: Write only enough production code to pass the failing test.
Procedure
- Single assert rule – sometimes called Triple (A) rule
Arrange: Create the data and context for the test; it’s usually done or at least partially done in the setup functions.Act: Calling the function to be tested, and the Assert is not physically single but we want it logically single. Meaning, want to Arrange Act Assert not Act Assert Act Assert Act Assert in a single test case, so we can have many assert statements in single unit test but to test only single logical action.
Assert: Verify that the function being tested does what’s supposed to be done. This is done by some kind of assertion.
So to recap the rule we can say we want every action tested independently. We don’t want the input of one test to be the output of the previous test.
- Incremental algorithm
As the test gets more specific, the code gets more generic. Practice TDD as playing golf game; try to pass the failing test by the smallest number of keystrokes.If you write the tests in the correct order and continuously generalize the production code (by starting with degenerate tests first), the algorithm will write itself.
Getting STUCK is a technical term that means there is nothing incremental you can do to make the currently failing test pass. Getting that test to pass causes you to write the whole algorithm, so getting stuck is a symptom of a problem. Maybe you’re writing the wrong test or maybe you’re making the production code too specific, or maybe both.
- Clean tests
Every test function has several phases like Arrange, Act, Assert and AnnihilateArrange phase
In XUnit patterns book, Gerrard Mezarros called the state of the system made during arrange phase “Test Fixture” and he describes three different ways to manage it.
When tests get complex, it won’t be easy to manage tests fixtures. Most systems have suites of tests containing hundreds of test functions.
Test Fixtures Management
1st way: Transient Fresh, which means that test fixtures are created and destroyed for each test.
2nd way: Persistent Fresh fixtures survives from one test to another but it’s completely initialized for each test.
3rd way: Persistent Shared, like persistent fresh, but allows some state to accumulate from test to test.
Setup struggles
Test setups can get very large. It is mandatory to use the test contexts in JUnit, so that we can make setup methods for related test context without polluting the scope or context of other test methods.
Action phase
This phase is where the behavior we want to test is executed. There are times that a single function is not sufficient to represent the single behavior being tested (this is an indicator that the code of this action is not in a functional style).
The fact that the system itself doesn’t have an operation that composes these functions doesn’t mean that the tests can’t have one; compose your actions.
Assert phase: Single assert rule
Test is a boolean operation that has only one result: True or False. This doesn’t say that we should have only one assert statement, but we need a single logical assertion, not physical assertion. So it’s ok to have many assert statements in my test function as long as there is only one action first.
This leads us to the assertion composition to become a single well named composed function. Remember that tests are supposed to be a clear and simple specification of intent, not messy coding details.
- Solid tests
Tests that know too much are fragile because they depend on irrelevant information. This information could be changed later during refactoring. So if the test depends on them, then the tests will need to be modified whenever this information is changing. Try to decouple your tests from irrelevant knowledge. Treat your tests as your production code.Naming the tests
Arrange —-> GivenAct —-> When
Assert —-> Then
We can name the arrange functions for the “Given” part of the test, the action functions for the “When” part of test, and for sure the assert functions for the “Then” part of the test.
When there is a number or any other thing like it, use its significance in the name of tests (In the GivenWhenThen functions) instead of writing the number as it is in the test name.
“Remember we want the test to be low level documentation”
We want our test functions to remind us of the business use cases or the requirements.
Different Techniques for TDD Practicing
- Fake it till you make it.
- Stair step tests.
- Assert first.
- Triangulation.
- One to Many.
- Mainly these techniques help for incremental algorithmic
Mocks and Test doubles
Gerard Meszaros published a book to capture patterns for using the various Xunit frameworks. One of the awkward things he’s run into is the various names for stubs, mocks, fakes, dummies, and other things that people use to stub out parts of a system for testing. To deal with this, he came up with his own vocabulary which I think is worth spreading further.
The generic term he uses is a Test Double (think stunt double). Test Double is a generic term for any case where you replace a production object for testing purposes. There are various kinds of double that Gerard lists:
- Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
- Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example).
- Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what’s programmed in for the test.
- Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages were sent.
- Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don’t expect, and are checked during verification to ensure they got all the calls they were expecting.
Different schools of thought about TDD
The school that tolerates higher coupling between test and production code for the increased assurance. I.e. it spies on the algorithm as we could not test all the inputs.
The school that emphasizes on the value returned by functions and prefer to keep tests decoupled from algorithms implementations.
Both are useful. For example, spying could be very useful when you are testing things that cross the dependency inversion boundary of the system, because the things on the other side of the boundary are the things we want to mock out. On the other hand, if the test was not on the other side of the boundary, decoupling would be better.
Mocking Patterns:
From the useful mocking patterns that you can read about is:
Testing specific sub-class when you wish to test the behavior of a function of some class but with altering or limiting the behavior of other functions on the same class; “Stubbing and spying on its self”.
Self-shunt when the test itself implements the mocked functionality.
Humble object http://xunitpatterns.com/Humble%20Object.html
Materials & Resources
Books and articles:
Clean Code for <Robet C. Martin>
XUnit Patterns for <Gerrard Mezarros>
TDD for <Kent Beck>
Growing OO software guided by tests for <Steve & Nat>
https://martinfowler.com/bliki/TestDouble.html