Home >
Real-time Mantra > Object Oriented Design
> Unit Testing
Unit Testing
These vidoes from Misko Hevery describe how we can write testable code. They
also explain how the act of writing testable code improves the design.
What makes code hard to test?
Using new operator in business logic
Looking up other objects in business logic
Doing work in a constructor
Global state/singletons
Static methods (essentially procedural programming)
Deep inheritance trees
Too many conditionals
What makes code easier to test?
Class dependencies are explicitly specified. This helps the unit
test code instantiate a small subset of fake objects to verify the class
under test
Business logic and object graph construction logic are completely
separate
Dependency injection
Comparison of different test techniques:
Attribute
Scenario testing
Functional/Subsystem testing
Unit testing
Scope
Test the system as a user.
Replace the external dependencies of the subsystem
with dummy entities.
Test individual classes
Speed of debugging when a test fails
Takes a long time to identify and fix the bug. Need
to hunt for the bug in the entire system,
Identifies the bug right down to the subsystem
level. It takes time to identify the bug within the subsystem.
Identifies the bug right down to the method level
Speed of execution
Slow and time consuming
Moderate
Very fast
Reliability
Test are flaky. Many times the tests might fail due
to issues with the test environment
Less flaky than scenario tests. Issues may be
encountered due to dummy interface entities
Tests are crisp; less chance of tests failing due to problems
with the test environment
Ease of writing tests
Hard
Moderately difficult
Easy
Volume of tests
A small set of scenario tests to verify that all
subsystems operate together.
A larger set of tests that identify the working of a
subsystem.
A large set of tests. The test code almost equal in size to the
production code.
VIDEO
Singletons/global variables make code hard to unit test
For unit testing to be reliable, we should write code where
repeating an operation produces the same result.
If performing an operation results in modification of a global
state, it is hard to guarantee that repeating the operation will produce
the same result.
Use of singletons results in a deceptive class interface as the true
dependencies of the class under test are buried inside the code.
Each unit test wants to instantiate a small portion of the full system
and run a test. When instantiating a small part results in side effects,
execution of a test might create side effects that will result in other
tests to fail.
Global state is transitive, any object you obtain from from a global
object is deemed global.
The presentation contains a detailed example that illustrates the ill
effects of having a global state.
VIDEO
Explicit new calls inhibit unit testing
The unit tests cannot use dummy and mock objects for dependencies of
the class under test
Dependencies should be passed explicitly to constructors
The constructors should just store the references the dependencies; they
should not be calling methods on the dependencies
The Law of Demeter states that classes just call methods on their
dependencies
Obtaining other objects from dependencies is prohibited
Bursting dependency injection myths
The business logic and object graph construction should be completely
decoupled
The business logic classes get their dependencies via
constructors/methods
Business logic objects are not aware of the object tree
No explicit new statements are present in the business logic
The factory classes are responsible for creating objects with
explicit
The factories also make sure objects are created with valid
dependencies
When a new object is requested, the dependencies for that the
object might also be created by calling the factory methods for the
dependencies.
Dependencies passed in the constructor should have a lifetime that is
same of greater than the object being constructed.
Frameworks like Guice can be used to automate object graph construction
and factories.
VIDEO
Most ifs in the code can be replaced by polymorphism
With procedural programming, we saw the end of "goto".
Object oriented programming should lead to elimination of most
conditionals in the code
Exception: Comparisons using primitive types on both sides cannot be
replaced with polymorphic behavior
Code without conditionals is easier to read and test
There is a fine line here. Deep inheritance hierarchies make the
code harder to test and maintain.
A few rules for reducing conditionals:
Save callers from checking the return value:
Do not return NULL from objects, when no operation is desired,
return a dummy do nothing object.
Throw exceptions instead of returning error codes from methods.
"if" statements checking the type of the object can be replaced with
polymorphism
A switch statement almost always can be replaced with polymorphism
Multiple if statement checks in the code point to missed polymorphic
modeling.
In many cases, use of polymorphism shifts an if statement from the
business logic to the factory code
This improves readability and testability
Performance is also improved, as the if check is being performed
only at the time of object construction
Dependency injection frameworks like Guice can take care of
factory if statements
VIDEO
Add tests to code after writing is like trying to add sugar to a cake
after baking it!
Tests written after the code tend to contain a lot of implementation
level details.
Such tests are very hard to maintain.
The tests don't read like a story
Writing tests before the code solves these problems
Tests should be viewed as executable specifications
This is also referred to as BDD (Behavior Driven Development)
Each test tells a story
The tests are written in a behavior
Humans are good at generalizing if they are given specific examples
on something works. Executable specs serve as the examples for
understanding code.
End to end tests
Principles that apply to unit testing also work for end to end
tests. Components are the entity being tested at this level.
Dependency injection at component level would simplify end to end
testing
End to end tests tell more complicated stories. An English type DSL
helps in telling these stories.
VIDEO