Introduction to TDD

Why test an application? Because it's the best way to find defects in software. Testing does not guarantee the application is free of bugs, but it allows us to detect them. Testing helps us create a higher quality product.

A well written test gives us confidence to refactor the code it tests; if we break it, the test will fail.

1 - A taxonomy of tests

  • Unit tests:

    They test a code block in isolation from its environment (project, solution, package, etc). In their most purist form, unit tests should not access any external dependency such as the file system, databases, external web services, special hardware, etc. This can be achieved via various techniques that we'll look at in a later post. The main reasons for these restrictions are to have real isolation of the test unit, and keeping unit tests fast. Fast unit tests are important in order to keep the TDD cycle fast and productive; if we bog them down with I/O, they will quickly become a bottleneck when they grow in number. One of the primary purposes of this kind of test is to isolate bugs in order to ease their detection and correction. This is the most important type of test when doing TDD.

  • Integration tests:

    These test a block of code without isolating it from its environment. They suppose that the components (classes, packages, etc) which interact with the tested code where unit tested. These tests aim to test the interaction between components and provide a guarantee that they work well together.

  • Regression tests:

    Test that functionality already tested in previous versions still works in a new version.

  • Stress tests:

    Analyze the application's behaviour under critical conditions: Millions of input files, hard real time requirements, etc. Such conditions will heavily depend on the particular application.

  • Functional tests:

    Test that the application satistifies the requirements. These tests are usually driven by the Use Cases specification, or a similar requirement format.

  • Acceptance tests:

    A special case of functional test, in which functionality is tested manually. This is typically done in front of the customer/stakeholder, and if the tests pass, the functionality is accepted as delivered and concluded.

2 - A test as the implementation of a use case

This concept is the cornerstone of TDD, but it's dificult to assimilate for developers who didn't learn to program right off the bat using TDD. This is because they are used to implementing first and testing later. TDD proposes a total reversal of that paradigm: We test first, and implement later.

TDD conceives a test as the way to write a requirement in a non ambiguous language: source code. TDD uses the test as a specification of the component to develop and not as a mere means to verify implemented functionality. Since a test is no more than a spec, we shouldn't write any more code than the minimum required for the test to pass. Writing more than that might introduce code which is not covered by a test, something which we must avoid in this initial stage.

Therefore, in order to write the tests we need the formal requirements, usually in the shape of use cases or user stories, though there are other possible formats. Once we have the requirements, a possible approach would imply writing a test for each use case. This works fine as long as all use cases have the same order of complexity. If that is not the case, complex use cases might need to be decomposed, ideally in independent sub use cases. Whatever the case, it's important that each method in a test class is simple, short and self-documented. Writing the tests before implementing the functionality helps to achieve tests like that.

Once tests are written, our only concern is that they pass. At this point, we enter the TDD cycle:

The first time we go from 1 to 2, we must do the minimum effort for tests to pass, and nothing else. Good practices can be temporarily ignored: it's acceptable to return constants, blindly copy-paste, duplicate code, switch by type, etc. Follow the shortest path. This is another aspect of TDD which might shock newcomers. The important concept is that once tests pass, we are covered and can refactor at ease. In the ulterior refactoring cycles, we can apply good practices.

To be Continued...

Spanish version / Versión en español

Comments

Popular posts from this blog

VB.NET: Raise base class events from a derived class

Apache Kafka - I - High level architecture and concepts

Upgrading Lodash from 3.x to 4.x