|
TDD_Overview
OriginsTest-driven development (TDD) has existed since the 1960’s, when it was used in a rudimentary form by NASA on Project Mercury, but more recently has been popularised by Kent Beck as a tenet of Extreme Programming (itself a component of Agile software development). TDD is nowadays considered a development approach in its own right, facilitating both contemporary software development practices such as continuous integration and pair programming, as well as ameliorating older issues like refactoring, and even providing in situ documentation. The ProcessIt is an evolutionary technique for programming that consists of writing test cases outlining the intended functionality of the code, writing code that pass these tests, and finally refactoring to either add more features, clean up the code, and/or improve performance. It must be stressed that TDD is not simply a mechanism for testing code, but one that allows developers to implement solutions that reflect the problem-domain (sometimes referred to as test-first design - TFD), without the influence of dependencies such as User Interface (UI) or Interaction Design (IxD) specifications. Traditionally programs were designed either on paper or by consensus verbally, functional code was then authored, and tests added as an after-thought if at all. This is especially true in the context of JavaScript programs, used ubiquitously online, and increasingly offline in desktop applications where rich interfaces are now de rigueur. In this scenario developers are reactive to broken code and regression errors, often having to second guess themselves, or maintain a mental checklist of all functionality (especially difficult when re-visiting legacy code). Even worse, developers don’t want to write rigourous test-suites that might break code they have spent days working on. TDD allows developers to be proactive in mitigating the introduction of bugs into their code, as well as providing a means to quickly detect regression errors (by the creation of a test suite). Ancillary benefits of TDD include better initial program design (more granular, focused, callable and testable), increased productivity and confidence for programmers, quicker development cycles, and a maintainable codebase for future development. If it’s worth building, it’s worth testing (Ambler)The hardest aspect of TDD to put into the practice is the test-first, implement-second mindset. The golden rule is that 'no functional code is implemented until there is a test for it'…the idea being that the test is witnessed to have failed. The concept of writing tests first is harder than it sounds, and requires discipline not to ‘slip’ and insert trivial code with no associated tests, but it is imperative the rule is adhered. Bob Martin puts it thus, “The act of writing a unit test is more an act of design than of verification. It is also more an act of documentation than of verification. The act of writing a unit test closes a remarkable number of feedback loops, the least of which is the one pertaining to verification of function”. Whilst some go as far as to not author even the containers for code, such as classes and interfaces, in practice it may be preferable to simply stub functions/methods/interfaces so that they do nothing at first. This is especially useful when utilising design patterns. On the subject of writing tests, practicing TDD assumes an appropriate testing harness is available to run the tests, otherwise it would be very difficult. Test harnesses normally comprise of a test runner to provide mechanisms such as the ‘setup’ and ‘teardown’ phases, and a DSL allowing you to formulate the abovementioned test cases in given programming language (often the same one in which the system under test SUT is written) in the form of assertions. More information pertaining to JavaScript test harnesses, runners and DSLs is available here (=>). The TDD process follows this outline:
TDD ApproachesTDD can be implemented in a number of ways that aren’t mutually exclusive. Typically test runners structure tests cases in the following manner:
The two most common TDD approaches are referred by Matin Fowler as ‘Classical’ and ‘Mockist’ in his seminal essay ‘Mocks Aren’t Stubs’, and represent different methodologies for verifying the result of an Exercise Phase, in addition to defining the fundamental relationship between test code and the SUT. They facilitate the testing of both production logic, and business logic, as well as guaranteeing a total separation of concerns (in other words, test code is completely separate from the SUT, and even conditional compilation should be avoided). Classical TDDThe dominant approach, synonymous with unit testing on one element of the SUT at a time, and which uses state verification to ascertain code success, i.e. the post-Exercise phase state of the SUT and its collaborator objects. It best suits testing methods or functions that perform getter and setter operations (e.g. A database or the DOM), or format/serialise data (e.g. creating JSON or String formatting), and that are defined as part of an interface. Unit tests don't care how the functionality has been implemented (i.e. the internals of a method), just that it has an output that can be queried, tested and verified somehow. This is also known as 'black box testing', and once in place aids refactoring greatly since the internals of a method can be implemented in whatever manner the developer deems, as long as it is produces the expected outcomes. Black box testing doesn’t ‘observe’ the code itself, and so private features (methods/constants) that are interacted with within the SUT are implicitly tested by unit tests. This if acceptable, as refactoring is a chief component of TDD, and potentially abstracting or altering this private code should no*t break the test. Mockist TDDUnit tests are fine for testing disparate methods or functions in isolation from one another, but most programs also contain some business logic which affects not only which methods are invoked, but the order they are done so, and the parameters that get passed to them. These modules of code also often require external dependencies, or collaborator objects, in the form of 3rd party libraries, global modules (system-wide code), or remote services (APIs). For JavaScript even the Browser or Document Object Models (BOM & DOM) respectively can be thought of as a dependency, as scripts will often perform lookups to them to assess data and inform the decision-making process. The Mockist approach allow developers to write tests that use behaviour verification to assess the business logic of programs, and verify the behaviour is correct, without the collaborator objects having to be present. These are known as 'glass box' or integration tests, where the code is actually observed, but the system state is NOT mutated. The alternative variety of integration testing, ‘white box_’ testing, is discouraged as it does mutate system state, and as such tests themselves can introduced bugs into the SUT, resulting in false positive assertions. Not only is integration testing useful for acting as a regression pack to ensure that the business logic of a program is functioning correctly post-refactoring, it also encourages a better system design, in addition to loosely-coupled, reusable, modular code. However, it is less used for a reason, as they can be troublesome to write and can become too tightly coupled to any given implementation, meaning that if the SUT changes then the corresponding test may have to change. This should be avoided at all costs. You can find out more about practical implementations of unit and integration/functional tests here (=>). References
|
Sign in to add a comment