In these last years, since the first unit testing frameworks were made available, and methodologies like TDD have become mainstream, unit testing is turning into a more popular strategy in software development.
The main advantages that unit testing can bring into a software development project can be summarised in mainly two purposes:
- Design purpose. Help programmers creating new code.
- Correctness purpose. Make sure that the behaviour of the different independent small parts of the newly created code are correct.
But what usually gets overlooked is that unit testing has also risks involved, specially when applied using dogmatic approaches like “every public method should be unit tested” or “everything needs to be design so that is easily unit tested”.
The main risk with unit testing appears when too many unnecessary tests are created. This risk becomes obvious when every time that the code is changed the time spent fixing tests is way too high, hence impacting in the productivity of the developers.
The key to effectively use unit tests is to find a balance between your tests and the amount of time you need to maintain them. When looking for this balance is important to remember a few principles to ensure that you write efficient unit tests.
1. Behaviour is the key element to test.
Focusing in testing behaviour is the key to produce a good unit test. This way if you refactor the logic inside a method without breaking its behaviour you should keep all the related tests passing.
2. Not every method is applicable for unit testing.
There are many dogmas in agile, and to have a unit test for each public method seems to be one. While is true that if possible a method should be unit tested, if you cannot test its behaviour, is better to leave it alone. (See point 1) Unnecessary unit tests are going to lock you down from changing your code for no good reason, (See point 5). These areas of your code that can´t be unit tested should be tested from an Integration or manual perspective. (See point 4)
Some clear examples of these types of methods are those that encapsulate calls to a framework, methods that loop through a list of items and then delegate to a different method, methods that log out…
3. Unit tests which are not useful anymore should be deleted.
Tests are created mainly in two fashions:
- After the code is completed: These tests are created to built in an automated check for correctness in the associated methods. Creating useless tests (See point 1 and 2) may be made by mistake, if so, you shouldn’t feel any shame of deleting these tests, or if possible to refactor them to focus on the behaviour.
- Before the code is completed. (Specially known for TDD). In these cases, tests have a second goal, to help programmers to come up with a cleaner code. After the code is complete, is usually a good idea to review the tests created and to delete/refactor them wherever applies. This unfortunately never made it to the list of steps to follow in TDD…
4. Unit tests will never substitute manual and integration testing.
Unit tests, once that your code is completed, help you diagnose weather the individual parts of your application are working as you expect. This is important, but is very far away of proving that your application is robust and works according to your customer expectations, which is your main goal.
Units tests are only a small part of the complete picture, you are going to need integration tests for areas of your code where unit tests can’t prove their behaviour, and you are going to need manual testing in areas where you can’t create an automated test, or for more abstract areas like, usability and UI testing.
5. Unit tests that lock down your code from changes are evil.
If there is one particular type of test to be avoided at any cost, these are the tests that lock down your from changes without adding any value. Let me illustrate this with some pseudo-code:
MyClass.MyMethod (magicParam1, magicParam2) START magicReturnValue = someOtherClass.doSomething (magicParam1, magicParam2) veryRemoteClass.stuff (magicReturnValue) managerOfManager.buzzinga(magicParam2) END
MyTestClass.MyMethodTest START when (someOtherClass.doSomething (magicParam1, magicParam2)). thenReturn (magicReturnValue) myClassToTest.MyMethod (magicParam1, magicParam2) verifyICalledThis (someOtherClass.doSomething (magicParam1, magicParam2)). andReturnedValueIs (magicReturnValue) verifyICalledThis (veryRemoteClass.stuff (magicReturnValue)) verifyICalledThis (managerOfManager.buzzinga(magicParam2)) END
What is the previous test achieving? Well, is actually achieving a lot… of pain… This is the one thing that sets me off when I see others people code, not only this is not proving anything about the expected behaviour of your code, but if someone refactors the main class maintaining the same logic, he will find that this test fails miserably, only because the code changed, not because there is any unexpected change of behavior in the code…
Funny thing about this type of tests, at least for my experience, is that they are usually most defended by the extremist agilist, everything must be unit tested… Have they perhaps forgot about their beloved agile process motto?