In recent weeks, I have been reading and trying out the concepts of polyglot persistence. In my upcoming posts, I will write more about my experience in this area. Today, we will talk about a common question that arises for many developers – when do I write my Unit Tests? or Is Test Driven Development (TDD) right for me?
When you search for TDD online, you find many links “for” the paradigm, and some “against”. The primary reason given by the folks who are against TDD, state that if followed strictly, TDD goes against concepts of “good” software architecture, for the application you are trying to write. This is the same argument that is given against Agile development as well. The point made is, the developer is to “too focused” on what they are solving “right now”, and doesn’t keep into consideration the “big picture”
While I do not want to start a holy war on this post, I would like to state my process. Those who know me would agree that I am a big picture focused person. The way I use Agile and TDD is not to start with a small piece, but “design” as much as possible based on the big picture I know (ask, ask, ask!!), and then start writing code. So in short – Planning using Big Picture (as much as possible, knowing things would change), and Execution using Agile and TDD.
The reason I choose to write my test cases first is so I am forced to answer the following questions:
- What business logic needs to be implemented to match up to the user story I am working on? – NEED
- For each business logic item, what are all the possible requests I would be making (knowing everything I know “now”)? – COVERAGE
- For each possible request, what is the result I expect the system would return me (knowing everything I know “now”)? – EXPECTATION / Success Criteria
With the above definition in place, lets walk through an example. As mentioned, Polyglot persistence application, meaning that the data will be stored in multiple databases (SQL and NoSQL). I will use a future post to describe some of the design decisions I made for my application.
The example that I am providing, comes at a point in the development, where I have already designed the high level database (DBMS agnostic), and written some modules. I have also made decisions on what data will go into which database.
At this point, I am ready to start writing by “Org” module. This module encapsulates the business processes for managing companies in my system. The following is a screenshot of how my Visual Studio 2013 project looks like right now:
The different project you see in the above screenshot depict the multi-layered code architecture in my application. The application contains business service functionality in the Domain.Service projects, and any section that directly integrates with any database, lies in the DataAccess.Repositories projects.
I start by writing an interface for the new org service:
The above interface defines functionality that may need one or more calls to the database, but would need other data manipulation in between. Direct interaction with the databases would be handled by the following repository interfaces:
At this point I am ready to define my test cases (answering the questions specified above):
As you can see, for the use case “Status Change” I expected two requests, hence two test cases. The idea now is to write tests that will fail. This would give us the indication to write code later on to manage the failure, thus fulfilling our “expectation”. To do this, we write implementation for the various interfaces we defined above, with just enough code so it can compile.
As you can see in the above implementations, any requests made to these would definitely fail. Also, keep in mind that OrgSqlRepository connects to an SQL database, and OrgUserHistoryMongoRepository connects to MongoDb. More on this in future posts.
Now we write one test.
As you see in this code, we use Dependency Injection and Inversion of Control to make the call to the implementation classes. Then we process the test case by calling orgService.SetupOrg. Our “expectation” at this point is that a new Org will be created in the database, and the Id would be greater than 0.
When we run the tests, obviously it fails, since the implementation doesn’t have any processing code.
Now we start filling the holes in the implementation to “make” this test a success.
Now I run my tests again, and see success.
The example provided here is a simple one, but it shows the steps I follow. Does it add extra time to my development? – Yes, about an hour. What it gives me for that extra effort is a more thought out development process by forcing me to answer the questions specified above, and makes my code future proof for regression testing. By thinking about the test first, it makes any changes to the business logic months down the line more manageable.
Please let me know your thoughts and your process on writing tests.