Understanding Testability and its Drivers
Testability is a function of observability and controllability, and understanding how to develop software that has these two properties is critical, because it will affect both small and large design decisions. In addition, testability is also influenced by smallness: the number of features should be kept down, no code should be duplicated, correct idioms and patterns should be used, as well as the right level of abstraction and a programming language suitable for the problem domain.
On top of that, testability is also affected by indirect input and output, temporal coupling and state, and large domain-to-range ratio. Knowing how to identify and deal with all of the above is a skill critical to developer testing.
Unit testing constitutes a foundation pillar of developer testing. Unit testing gives developers feedback, allows them to drive the design test-first, and is the natural starting point for application of basic testing techniques. Knowledge about unit testing translates into test naming and structuring (arrange, act, assert), and employment of custom assertions and libraries.
Fundamental Testing Techniques
There are some basic testing techniques that pretty much every tester is familiar with. To developers, these techniques are extremely valuable, because they determine what and how many tests to write, as well as what types of test to use. Simply put, they provide a structured way to identify test cases. Sometimes the example-based nature of a typical unit test isn’t satisfactory, and a data-driven kind of test is a better choice. Applying testing techniques like, for example, boundary value analysis or state transition testing helps in identifying the appropriate test archetype.
Programming by Contract
Programming by contract brings to the table the notion of preconditions, postconditions, and invariants. Some languages have built-in support for verifying these at runtime, and many contract libraries exist for languages that don’t. Runtime verification is secondary from the vantage point of developer testing. What’s more important is the awareness of the contract building blocks and that unit tests and static analysis can be used to express contracts as well.
It’s not hard to find examples of tests that require many values to provide enough confidence. Therefore understanding data-driven testing—parameterized tests and theory tests—and eventually generative testing is crucial for developers who want to achieve comprehensive test coverage of their software.
Employment of Test Doubles
Technically, test doubles belong to unit testing. However, in developer testing, they play a larger role. First, understanding the function and application of the various kinds of test doubles is crucial to authoring understandable and meaningful tests. Second, not all languages come with well-developed mocking frameworks (or there may be some other constraints that prevent you from using such frameworks). Last, test doubles scale beyond unit testing. Entire systems may be replaced by stubs, mocks, or more likely, fakes.
At the end of the day, the test double will most likely be created using a mocking framework, so understanding what functionality they provide and accompanying pitfalls is a core competency.
Given that testability is a function of controllability, observability, and smallness (and not time and precedence), test-driven development becomes one technique among others. That said, it’s an effective technique to reach controllability and observability, and most of the time, smallness. Large systems require the use of both classic TDD and mockist TDD, which is why a developer proficient in developer testing will be able to alter between the two styles and pick the most appropriate when need be.
Understanding various types of Dependencies and Dependency Breaking
All programs are composed of building blocks (be they “classes,” “modules,” “units,” etc.) that somehow depend on each other. The exact type of dependency is dictated by the language. The book on developer testing has been written primarily with object-oriented languages in mind, but that’s just a limitation of the book. Knowing what dependencies look like, regardless of whether they occur between individual program elements or between layers or tiers, is critical to writing testable code.
Higher-level tests: Testing beyond Unit Testing
Developer tests that are more advanced than unit tests will obviously have different characteristics. They will be slower, exhibit a degree of environment dependence, use a different language, and provide worse error localization. At the same time, they may prove more meaningful to non-technical stakeholders if they’re authored so that they can be read by them. Writing maintainable tests with these characteristics is a skill in itself. As is identifying fast medium tests, which are tests that are as fast as unit tests, but require more moving parts and easily get coupled to the environment.
In addition to the core competencies, developers, who master developer testing, also know testing terminology, common testing models, and above all, how they fit into a developer’s daily work.