Striking Balance in The Test Suite
Test-driven development is about preserving current functionality when facing new requirements. Test-driven development is about keeping existing functionality when meeting contemporary needs. We aim for TDD to get as short a feedback loop as possible. We are seeking a flexible enough design for the feature at hand. All the changes we apply to any code base will depend on how we arrange the code in the face of new requirements. Requirements push the structure of an application to change every time we try to add a new one. Testing makes us think the new arrangement keeps the before-implemented behavior the same. A quick feedback loop cycle allows for experimentation around the same problem and helps identify the right solution.
While as a beginner, you shouldn’t worry much about what not to test on day one, you better start picking it up by day two. Humans are creatures of habit, and if you start forming bad habits of over-testing early on, it will be hard to shake later. Think of it like this: What’s the cost to prevent a bug?
Was it worth it if it took 1,000 lines of validation testing to catch the one time Bob removed the `validates_presence_of :name` declaration? The problem with calling out over-testing is that it’s hard to boil down to a catchy phrase. Testing what’s useful takes nuance, experience, and dozens of fine-grained heuristics. Don’t aim for 100% coverage. Code-to-test ratios above 1:2 is a smell, and above 1:3 is a stink. Don’t test standard Active Record associations, validations, or scopes.
Well-designed applications are highly abstract and under constant pressure to evolve; without tests, these applications can neither be understood nor safely changed.
Balanced test suites for long-term maintainability: Are your tests running slower than you’d like? Or are your tests brittle, making it harder to refactor and change your application functionality? The testing pyramid balances tests speeds up suites, and reduces change costs. It centers on the composition of different types of tests in your suite. A tested application feature requires both unit tests and acceptance tests. Start by making sure you have good unit test coverage. Your unit tests should catch edge cases and confirm correct object behavior. These will give you confidence that all objects are playing together. It’s important to realize that — like scaffolding — a test can be helpful without being permanent — the Throwaway Test. When you’re done, pare your test suite down to the most miniature set of tests to provide the confidence you need (which may vary depending on the feature).
Creating a testable system ensures the ability to make future changes. However, having a testable system may mean something other than solid design. It depends on the TDD knowledge of the code writer. The main benefit of a testable system is breaking down the problem into smaller, more manageable chunks. A better design is determined by comparing two solutions to the same problem. It is incorrect to assume that a system with unit tests has a better design, as the application may have other design-related issues. TDD leads to a better design by implementing small code chunks based on examples, leaving no room for speculation. Defining “better design” depends on the problem, but it should always move towards simplification.
Using TDD, it becomes clear if more complexity is being added. The relationship between good design and velocity is tight, and if the team’s speed cannot be measured or is slow, that implies issues with the test suite. The Test Pyramid suggests having as many unit tests as possible while reducing the number of other types of tests. This equates to a more accessible-to-unit test system being a better design system.
Design decisions are made every time code is written, whether using TDD or not. If the application is successful, some decisions may need to be changed later, not because of a lack of diligence but because untested systems are complex to test. The friction one might feel with testing something is more related to a bad design than TDD.
Discarding any pre-made patterns or assumptions about the future creates flexibility for change. Every change in an application is a chance to write code that fits into the existing structure. Good design is about simplification, and facing a particular pattern creates more friction when a new change is requested. Applying the unit test paradigm to areas of code that need to fit better makes issues and discards beneficial patterns.
[¹]: Practical Object-Oriented Design: An Agile Primer Using Ruby