Unit testing (Exploring the other side as a tester)
TestDrivenDeveloment
1. Test Driven Development
Objective
To realise productivity and quality gains by making extensive use of Test Driven Development
Abstract
(Abstract and TDD details taken from: http://en.wikipedia.org/wiki/Test-driven_development)
TDD relies on the repetition of a very short development cycle: First the developer writes an (initially failing) automated test case that defines a desired
improvement or new function, then produces the minimum amount of code to pass that test and finally refactors the new code to acceptable standards. Kent
Beck, who is credited with having developed or 'rediscovered' the technique, stated that TDD encourages simple designs and inspires confidence.
Proposals
Develop tests for functionality of components - rather than just to test one Jira, and keep them up to date.
Implement Continuous Integration either during the nightly build or on code check-in to SVN. Automated regression tests should enable us to develop new
code and refactor with confidence that we haven’t broken anything. If any test fails then we will break the whole build!
Incorporate in the Continuous Integration, code analysis tools such as 'Emma' (Check % of code covered by the tests) and 'Findbugs' (Static analysis of the
java code for bugs inc. concurrency issues)
Avoid the problem of tests written by the developer sharing the same blind spots with his code by having another developer, or better yet a tester, write them.
This last proposal is probably controversial but, I think, key to improving quality.
Whilst not going as far as 'pair programming' it would enable us to implement the 'four eyes' principle - at least two people would be independently involved in
the production of every unit of software.
Also, there is much less chance of requirements being misunderstood and any lack of clarity or misunderstanding that does occur can be caught earlier.
Ideally, before a line of code is written.
Finally, it should; help avoid 'scope creep', facilitate knowledge transfers and front-load the testing effort so we can identify problems earlier rather than at the
point of delivery. For example, if the requirements change, the tests will have to be re-written and everyone involved (management, testers and developers)
will be aware of the issue.
2. TDD Cycle
Add a test
Write a test which, must inevitably fail as the code implementation has not yet been written. As opposed to writing tests after the code is developed this forces
the developer to focus on the requirements before writing the code.
Run all tests and see if the new one fails
This validates that the test harness is working correctly and that the new test does not mistakenly pass without requiring any new code. This step also tests
the test itself ruling out the possibility that the new test will always pass, and therefore be worthless - it should fail for the expected reason!
Write some code
…that will cause the test to pass. This code will not be perfect and may, for example, pass the test in an inelegant way. This is acceptable because later steps
will improve and hone it. Important that the code written is only designed to pass the test. No further (and therefore untested) functionality should be predicted
and 'allowed for' at any stage.
Run the automated tests and see them succeed
If all test cases now pass, the programmer can be confident that the code meets all the tested requirements. This is a good point from which to begin the final
step of the cycle.
Refactor code
Improve the code as necessary to attain production quality standards. By re-running the test cases, the developer can be confident that code refactoring is not
damaging any existing functionality.
Remove any artifacts that were introduced - for example magic numbers or strings - in order to make the test pass in earlier stages
Repeat
Starting with another new test, the cycle is then repeated to push forward the functionality. The size of the steps should always be small, with as few as 1 to
10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert in
preference to excessive debugging.
3. Continuous integration helps by providing revertible checkpoints.
Code Visibility
Test suite code clearly has to be able to access the code it is testing. On the other hand, normal design criteria such as information hiding, encapsulation and
the separation of concerns should not be compromised. Therefore unit test code for TDD is usually written within the same project or module as the code
being tested.
There is some debate among practitioners of TDD, as to whether it is wise to test private methods and data.
Some argue that private members are a mere implementation detail that may change, and it should be sufficient to test any class through its public interface
or through its subclass interface. Others say that crucial aspects of functionality may be implemented in private methods, and that developing this while testing
it indirectly via the public interface only obscures the issue.
My view is that if the private members have no discernible effect on the public behaviour of a class then of what use are they?
Or to paraphrase Danny Blanchflower – if they’re not interfering with play, then what are they doing in the codebase?
In other words, if invoking all the public methods does not, in turn, invoke all the private methods then when may we expect them to be invoked – if ever?
Stubs,mocks and integration tests
When code under development relies on a database, a web service, or any other external process or service, enforcing a unit-testable separation is also an
opportunity and a driving force to design more modular, more testable and more reusable code.
Two steps are necessary:
An interface should be defined that describes the access that will be available.
The interface should be implemented in two ways, one of which really accesses the external process, and the other of which is a Stub or a Mock
Stub objects need do little more than add a message such as “Person object saved” to a trace log, against which a test can be run to verify correct behaviour.
Mock objects differ in that they themselves contain assertions that can make the test fail, for example, if the person's name and other data are not as
expected.
4. Shortcomings
Test-driven development is difficult to use in situations where full functional tests are required to determine success or failure, e.g. user interfaces, programs
that work with databases, and some that depend on specific network configurations.
Unit tests created in a test-driven development environment are typically created by the developer who will also write the code that is being tested. The tests
may therefore share the same blind spots with the code.
The high number of passing unit tests may bring a false sense of security, resulting in fewer additional software testing activities, such as integration testing
and compliance testing.
The tests themselves become part of the maintenance overhead of a project. Badly written tests, for example ones that include hard-coded error strings or
which are themselves prone to failure, are expensive to maintain. It is possible to write tests for low and easy maintenance, for example by the reuse of error
strings, and this should be a goal during the code refactoring phase described above.
There is a risk that tests that regularly generate false failures will be ignored, so that when a real failure occurs it may not be detected.
The level of coverage and testing detail achieved during repeated TDD cycles cannot easily be re-created at a later date. Therefore these original tests
become increasingly precious as time goes by. If a poor architecture, a poor design or a poor testing strategy leads to a late change that makes dozens of
existing tests fail, it is important that they are individually fixed. Merely deleting, disabling or rashly altering them can lead to undetectable holes in the test
coverage.
5. FindBugs
Abstract
'FindBugs' is an example of a static code analysis tool.
It does not run any test cases and in fact knows nothing about the logic of your code. It simply examines it, without actually running it, for what are generally
considered to be bug patterns and bad practices.
As part of a comprehensive programme of code reviews it can be invaluable to catch hard-to-spot coding errors. (Human code review is still necessary)
It is available under an open source licence from 'SourceForge' and takes only a few minutes to install.
Once installed you can use either the built-in GUI, command line tools or plugins (e.g. for Eclipse) to run tests. You need to tell 'FindBugs' where your source
and byte code are and any external classes or jars required to compile it.
Locating the external classes can be tricky if 'FindBugs' reports that it cannot check all your code because a particular class is missing.
Probably easiest to just add every jar/class on your classpath, to begin with.
Example Bug
This check was run against the entire java code base of an application and found 148 possible bugs.
These vary in seriousness and some discretion needs to be exercised in determining if any action is required.
This example is of synchronization using a 'ConcurrentHashMap' (a member of the java.util.concurrent package) as a lock object. Careful analysis is required
to decide if there are going to be a problems with this or not.
There is no point in locking using 'concurrent' classes as they are internally guarded against concurrent access in such a way as to allow multiple threads to
access them at the same time. E.g. in this case each hash bucket will be guarded by a separate lock.
Unless more than one thread tries to access the same bucket concurrently they will be allowed to read/write the map's contents (Except if this code snippet is
the only code that can access the map. That is not the case here as 'status' is exposed through a public method 'handleTick()' which employs no
synchronization when accessing it - correctly, of course)
If the program's correctness does not depend on only one thread at a time being able to access the map then there will probably be no problem with this. For
example, if it's just a case of being a bit over-zealous with the synchronization of something that doesn't need synchronizing.
If the correctness of the program does depend on exclusive access to the map then this code is broken.
At best, this indicates something that should be explained. Why try to lock something that cannot be locked? If you do want exclusive access then why are you
using a 'ConcurrentHashMap'? On a wider note, synchronizing access to ANY resource and then allowing public unsynchronized access to it through another
method, or by synchronizing on a different lock object, is not threadsafe!
6. Another issue...
NOT picked up by 'FindBugs', is in the public method 'handleTick()'. The return values of putIfAbsent() are ignored!
status.putIfAbsent(info.tickNum, new TickStatus(info.tickNum, info.instrId));
statusInstruments.putIfAbsent(key, new InstrumentStatus(key));
If a non-null value is returned from putIfAbsent() , then the put operation failed and the value returned is the value already associated with the key!