Most of us have at least heard about the DRY principle. But how is it applied in a pragmatic way in software development?
I will take you though my thoughts as I work with an application to illustrate how, when and why I apply it as well as where it may be better not to.
The code is in python. It is not a fully working application since it isn't open source.
The example application is a simple stock management application. It has a warehouses. The warehouse has sections and each section contains SKUs (Stock Keeping Units). It also has a catalogue with categories and products. Each product is bound to one or more SKUs.
I am using SQLAlchemy as ORM and this also maintains the database schema using declarative_base. In the tests I am using fixture to create data fixtures that can be injected to the database when executing the tests. Since the application is so simple SQLite is a sufficient database engine.
The first requirement is to create a function that will find a warehouse using a warehouse ID. The first step is to start the database and inject data into it. This is done in the setUp() method in the test like so:
The WarehouseFixture and SectionFixture are fixture.DataSet classes, Warehouse and Section are domain classes.
This makes it possible to query data through the DOA and then assert the results using the fixtures.
After completing the other tiers needed in the application to expose this as a JSon front end I need to create some functional tests. They will need to inject the data into the database in the same way that the DAO test did. Then the functional tests will validate the JSon response from the server using the fixtures.
I could, and I know some would, C&P the function calls above into a new setUp() method in the functional test. But doing so will disconnect the fixtures and DAO tests in the functional tests with the tests used in the DAO. Since the DAO tests and fixtures will evolve with the data model and database we don't want the functional tests lose that connection. Instead I pull up some functions from the DAO test case into stand alone functions. This can then be used from the functional tests as well as the DAO tests.
We now have generic functions that can be called in different, unrelated, tests. And the code is DRY.
Next it is time to add some new DAO tests. This DAO will be in a new module which handles the catalogue. Since the previous refactoring did not require decoupling from the warehouse, both DAO and functional tests test the warehouse, it took place within the warehouse dataaccess test module. Since this will create a new decoupled module the test modules should not be coupled. To enable this we create a utility module called data_fixtures with a new class called DataFixtureTestCase like so:
Since python supports multiple inheritance it is possible to subclass this class and AsyncHTTPTestCase when creating functional tests. In the test cases setUp() function setupDatabase() is called to set up the database and teardownDatabase() is called in tearDown() to tear it down. The setupDatabase() also ensures that the required properties have been set before setting up the database.
To keep the connection from functional tests to dataaccess the functional test module imports the engine, env and fixtures properties from dataaccess.
The above refactorings helps keep the code DRY whilst easy to understand. It also creates a place where common assertions bound to data fixtures can live.
It should be noted that when working with test DRY is not always your friend. If the tests loses any of it's expressiveness as "executing documentation" (tests is the first stop for understanding production code) expressiveness wins any day. This is actually reversed from production code which must be kept DRY at all times.