Friday, June 9, 2017

Unit Testing and Test Driven Development (TDD)

Since my time with GE, I've learned a lot about the Agile Software Development process.  It had always been something I was exposed with, especially throughout my doctoral studies.  I mean after all, I did build a medium sized simulation of agile requirements engineering called POM3.  So going forward, I was excited to dig around into the wealth of knowledge already out there surrounding the topic of unit testing.  And here's what I found out.

Overview


Unit tests are an essential part of test driven development (TDD) and is part of the Agile Paradigm for software development.  "Get something working now and perfect it later."  Unit tests provide the most basic form of testing available to the developer and they are a great boon in aiding the developer in implementing features and user stories.  Some benefits are listed here:
  • Very cheap to run given their unitary nature in a decreased complexity environment
  • Offers an entry point to otherwise complex code
  • Very easily facilitates automation as a form of code-validation upon code check-in
  • Can be very entertaining to see failing tests made passing
It can be difficult to convince project managers to the benefit of unit testing.  Why waste time writing unit tests?  The simple answer: it is an effective means to retire risk early, thus saving on project costs in the long run.  It is important to note though, that this kind of investment is not for everyone.  Unit tests require maintenance, and if requirements often change, then unit tests need to be updated.  There are ways to mitigate these hindrances, however, by writing good unit tests that usually lead to good design.

Reading

Kent Beck is one of the premier authors on test driven development and unit testing, having introduced the concepts in 1970.  Your first stop for unit test literature should be here, and its not even that large of a book:
Another great book on Unit Testing, and in .NET, by author Roy Osherove, which is a bit newer:
Martin Fowler is also one of the original authors of the Agile Manifesto.  You can also find a wealth of content on his personal website.  Also a co-author (along with Kent Beck, listed above) of a good read on Refactoring.

Summary: How To Unit Test

TDD proposes unit testing as an integral part of development.  Before even typing a single key stroke, you should sit down and think about the user story you're working on.  That's step one -- to create a list of tests that you want to run.  The list of tests should be driven to cover as many code paths as possible around the user story.  The list should also be sorted in easiest-first order, where easy is defined as requiring the least amount of code to make it pass.
Each unit test should basically contain a standard arrange-act-assert (AAA) framework.  The arrange section provides some setup code that enables the calling of tested functionality in the 'act' portion of the test, and finally, the 'assert' section validates the functionality.  Consider writing each unit test in a reverse fashion: write assertions first, then act, and then arrange.
Writing your first test should occur before writing any production code.  At first, it should be a test that doesn't compile properly.  If you haven't written anything on the production side, then you'll have to provide the basic skeleton necessary to get the test to compile.  When the test is working properly, you'll get your first failing test, and will need to make it green by adding the code necessary to pass the test.  With a passing test comes the time to do any possible refactoring to clean up the code on both the test and production code.  Any then you move on to the next test in the list, repeating this red-green-refactor flow.  As you develop more tests, remember that each previous one must also pass, and the opportunity to refactor will likely be greater.  Typically this process is known as "red-green-refactor".
To Recap:
  1. Come up with a list of tests
  2. Write a failing test
  3. Make changes to code to pass that test
  4. Refactor code and move onto next test in list

Principles

The following are a list of seven ideas that can help with making good unit tests.  This list of principles doesn't necessarily come from any single source – it is a list of principles that came together as a result of digging through dozens of them.  At the end of this page is a rather large list of links that point to all sorts of discussions on unit testing and TDD in general, and they are definitely a recommended read for anyone interested.

#1: Unit Tests Are Not About Finding Bugs

Unit tests are actually about preventing bugs.  This is most often emphasized by automating the tests prior to check-in of new code.  If any existing unit tests are failing, then the build server will not accept your code changes.  This is a preventative measure.

#2: Use Mocks and Don't Use Mocks

You should do both.  Tests should both validate the functionality of code as well as its behavior.  When not mocked, a method can be tested to see if it returns the correct result.  When mocked, the method is tested to see if it was called properly and with the correct parameters. Granted, in smaller projects, mocks may not make a lot of sense given the triviality of the project.  Equally so, larger projects may inhibit the use of not mocking, since mocked objects can help to reduce complexity and provide that necessary level of isolation in a unit test.

#3. Everything Public Must Be Tested

So don't make everything public!  This may be a tendency, but a good design will probably hide a lot of layers of private abstraction behind a public interface.  If you do that, then you only need to test the public interface.  A test-first design will probably facilitate this.  Remember that if a five public classes each have five public dependencies, then you need to test twenty-five things.  And if requirements change, the code changes and all of your unit tests also have to be updated.  That's a lot of work!  

#4: Use Less dependencies

Dependencies should also be tested.  So it makes sense that if you have a lot of them, that's a lot of work.  If requirements change, code must be changed and so too do all the tests.  A lot of dependencies may sometimes exist because of duplication, and a good unit testing practice is to refactor these under common public interfaces.

#5: Don't Mock Everything

To mock a class object, it must be public.  Recalling from principle #3, that could be bad if you mock everything.  There are good suggestions on when to mock.  File IO is one of these, but DB access is probably not.  One good rule of thumb is to mock across significant architectural boundaries, and not within.  Also, don't mock third party tools that are not owned, since they can change at whim.  Remember the purpose of mocks: isolation.

#6: One Behavior One Test

Do try to keep each unit test down to one single assert.  That may not always be possible, but the premise remains.  A "unit" test should be testing a single "unit" of your software.  That *usually* means that one project is one test project, one class is one test class, and one method is at least one test case.  If you have more than one asserts in a single test case, consider the possibility of splitting the test into two cases.

#7: DAMP vs DRY

In principle, favor DRY in production code and DAMP in test code.  DAMP stands for "descriptive and meaningful phrases".  DRY stands for "don't repeat yourself".  Probably coined by Jay Fields (https://leanpub.com/wewut).  These two are not necessarily opposites, and you should quite certainly always be removing duplication as a means to refactor in both production and test code.  More than being a drab sequence of statements in test code, there should be a higher emphasis on "easier to read" in test code.  This means you can escape some standard patterns of good production code design, such as having method names like "TestThatWhenAdding2And2YouGet4" and "SetupTheCalculatorSoThatICanControlItWithMyPrivateAPI".  The reason for this is that whenever a test case fails, it should be immediately and directly clear why it failed.