Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: first the developer writes an (initially failing) automated test case that defines a desired improvement or new function, then produces the minimum amount of code to pass that test, and finally refactors the new code to acceptable standards.
The process can be defined as such:
- Write a failing unit test
- Make the unit test pass
Repeat this process for every feature, as is necessary.
Flipping the Testing Pyramid in DevOps
In DevOps, we believe the software delivery process should detect flaws and bugs as soon as possible. To gain fast continuous feedback and quality software we use automated tests. Automated tests provide us with a safety net to ensure that when we add new features and incorporate bug fixes we do so without breaking existing features. In both Agile and DevOps the whole team is responsible for test automation (including development and operations). The test automation mix must include unit tests, service tests and UI tests. Although you should use all three categories of tests, the basis of your test strategy should be formed by unit tests. This requires that we flip the traditional testing pyramid shown below.
A unit test verifies that a function or set of functions “meets the acceptance criteria” – in other words, that the function(s) under test meet the requirements. Unit tests inspect both black boxes or white boxes.
Black box testing is different from white box testing. The kind of testing that you can perform on the code determines, among other things, the complexity of the unit test.
A black box test (“functional test”) is one in which you feed it inputs and verify the outputs without being able to inspect the internal workings. Furthermore, one doesn’t usually have information regarding:
how the box handles errors
whether your inputs are executing all code pathways
how to modify your inputs so that all code pathways are executed
dependencies on other resources
A white box provides the information necessary to test all the possible pathways. This includes not only correct inputs, but incorrect inputs, so that error handlers can be verified as well. This provides several advantages:
you know how the box handles errors
you can usually write tests that verify all code pathways
the unit test, being more complete, is a kind of documentation guideline that the implementer can use when actually writing the code in the box
resource dependencies are known
internal workings can be inspected
A unit test also incorporates a “test fixture” or “test harness”.
The test fixture performs any setup and teardown that the test requires. This might consist of creating a database connection, instantiating some dependant classes, initializing state, etc. The test fixture is one of the things that causes problems for unit testing. Non-trivial testing can require complex setup and teardown processes which in themselves may be buggy, time consuming, and difficult to maintain. Hence, the need for “mock objects”.
Mock objects are things that simulate complex objects with simplified functionality and make it easier to create a test harness. I’ll go into mock objects in detail later, because to use them requires certain up-front design decisions to be made that are system-wide issues. But for now, simply keep in mind that unit testing often requires mock objects to simulate hardware, connections, or other resources that may not be available to the test harness. Unit tests also require mock objects for performance reasons – interfaces to production objects may be overly complex (requiring to much setup and teardown) and/or the production objects degrade the performance of the tests. Since unit tests are typically run very frequently, test performance is a factor. Find out more about Unit Testing.
In TDD, each new feature begins with writing a test. This test must inevitably fail because it is written before the feature has been implemented. To write a test, the developer must clearly understand the feature’s specification and requirements. This is a differentiating feature of test-driven development versus writing unit tests after the code is written: it makes the developer focus on the requirements before writing the code.
Now the code should be cleaned up as necessary. Move code from where it was convenient for passing the test to where it logically belongs. Remove any duplication you can find. Make sure that variable and method names represent their current use. Clarify any constructs that might be misinterpreted.