The team thinks about test environments and what to use for test data. Unit tests usually work with fake or mock objects instead of actual databases for speed, but programmers still need to test against realistic data. Testers can help them identify good test data. If the unit tests represent real-life data, fewer issues will be found later.
Lisa’s Story
Here’s a small example. When my current team first adopted agile development, we didn’t have any automated tests. We had no way to produce a deployable code package, and we had no rudimentary test environments or test databases. I didn’t have any means to produce a build myself, either. We decided to start writing code test-first and committed to automating tests at all levels where appropriate, but we needed some infrastructure first.
Our first priority was to implement a continuous build process, which was done in a couple of days. Each build sent an email with a list of checked-in files and comments about the updates. I could now choose which build to deploy and test. The next priority was to provide independent test environments so that tests run by one person would not interfere with other tests. The new database expert created new schemas to meet testing needs and a “seed” database of canonical, production-like data. These schemas could be refreshed on demand quickly with a clean set of data. Each team member, including me, got a unique and independent test environment.
Even before the team mastered TDD, the adopted infrastructure was in place to support executing tests. This infrastructure enabled the team to start testing much more effectively. Another aspect of trying to automate testing was dealing with a legacy application that was difficult to test. The decisions that were made to enable TDD also helped with customer-facing tests. We decided to start rewriting the system in a new architecture that facilitated testing and test automation, not only at the unit level but at all levels.
—Lisa
Writing tests and writing code with those tests in mind means programmers are always consciously making code testable. All of these good infrastructure-related qualities spill over to business-facing tests and tests that critique the product. The whole team is continually thinking of ways to improve design and make testing easier.
Designing with Testing in Mind
One advantage of driving development with tests is that code is written with the express intention of making the tests pass. The team has to think, right from the beginning, about how it will execute and automate tests for every story it codes. Test-driven development means that programmers will write each test before they write the code to make it pass.
Writing “testable code” is a simple concept, but it’s not an easy task, especially if you’re working on old code that has no automated tests and isn’t designed for testability. Legacy systems often have business logic, I/O, database, and user interface layers intertwined. There’s no easy way to hook in to automate a test below the GUI or at the unit level.
A common approach in designing a testable architecture is to separate the different layers that perform different functions in the application. Ideally, you would want to access each layer directly with a test fixture and test algorithms with different inputs. To do this, you isolate the business logic into its own layer, using fake objects instead of trying to access other applications or the actual database. If the presentation layer can be separated from underlying business logic and database access, you can quickly test input validation without testing underlying logic.
Layered Architectures and Testability
Lisa’s team took the “strangler application” approach to creating a testable system where tests could be use to drive coding. Mike Thomas, the team’s senior architect, explains how their new layered architecture enabled a testable design.
A layered architecture divides a code base into horizontal slices that contain similar functionality, often related to a technology. The slices at the highest level are the most specific and depend upon the slices below, which are more general. For example, many layered code bases have slices such as the following: UI, business logic, and data access.
Horizontal layering is just one way to organize a code base: Another is domain-oriented slices (such as payroll or order entry), which are generally thought of as “vertical.” These layering approaches can be combined, of course, and all can be used to enhance testability.