The Art of Unit Testing
The basics
Definition
- A unit test is an automated piece of code that invokes the unit of work being tested, and then checks some assumptions about a single end result of that unit.
- A unit of work is the sum of actions that take place between the invocation of a public method in the system and a single noticeable end result by a test of that system.
A noticeable end result can be observed without looking at the internal state of the system and only through its public APIs and behavior
The invoked method returns a value
A noticeable change to the state or behavior of the system before and after invocation that can be determined without interrogating private state
There is a callout to a 3rd party system over which the test has no control
Properties of a good unit test
It should be automated and repeatable
It should be easy to implement
It should be relevant tomorrow
Anyone should be able to run it easily
It should run quickly
It should be consistent in its results
It should have full control of the unit under test
It should be fully isolated
When it fails, it should be easy to detect what was expected and determine how to pinpoint the problem
- Integration testing is testing a unit of work without having full control over all of it and using one or more of its real dependencies, such as time, network, database and so on
- A regression is one or more units of work that once worked and now don't
- Control flow code is any piece of code that has some sort of logic in it. It has one or more of the following: if, loop, switch, calculations or any other type of decision-making code
Test-Driven Development
1⃣ Write test - at this state the tests will fail
2⃣ Make test pass by writing production code
3⃣ Run all tests - they should pass otherwise the solution should refactored
- Refactoring means changing a piece of code without changing its functionality
Unit test structure
1⃣ Arrange objects
2⃣ Act on an object
3⃣ Assert that something is as expected
Best Practices
One test class per tested class
Name your tests clearly with the following model: [unitOfWork][Scenario][ExpectedBehavior]
Use Factory methods to reuse code in your tests, such as creating and initializing objects
Don't use setUp()
and tearDown
if you can avoid them, since they make tests less understandable
Unit Test Types
Value Based
Testing objects that relies on another object over which you have no control
The problem here is that your test cannot control what that dependency returns to your code under test or how it behaves
Definitions
- External dependency is an object in your system that your code under test interacts with and over which you have no control.
- A stub is a controllable replacement for an existing dependency in the system. By using a stub, you can test your code without dealing with the dependency directly.
⭐ You can't test something?
- Add a layer that wraps up the calls to that something and then mimic that layer to your tests or
- make that something replaceable (so that it is itself a layer of indirection).
How to test easily
1⃣ Find the interface or API that the object under test works against.
2⃣ If the interface is directly connected to your unit of work under test, make the code testable by adding a level of indirection hiding the interface.
3⃣ Replace the underlying implementation of that interactive interface with something that we have control over, a stub.
- Seams are places in your code where we can plug in different functionality. Seams are what we get by implementing the Open-Closed Principle. Examples:
- Adding a constructor parameter
- Adding a public settable property
- Make a method virtual, so it can be overridden
- Externalize a delegate as a parameter or property so that it can be set from outside a class.
✅ You can refactor by introducing a new seam into it without changing the original functionality of the code
⚠ make sure that the resulting code does exactly the same thing it did before
Abstracting concrete objects into interfaces or delegates
Refactoring to allow injection of fake implementation of those delegates or interfaces
Inject stub implementation into a class under test
Inject a fake at the constructor level
Inject a fake as a property get or set
Inject a fake just before a method call
Solution: Use stubs
State Based
Interaction
The method under test is returning a value which is evaluated against an expected result
The method under test change the state of the system. So the state before and after the method invocation is evaluated
Is testing how an object sends messages to other objects
It's an action-driven testing, which means that you test a particular action an object takes.
Always choose an interaction testing as the last option
On interaction unit tests we are testing how many times a method called
❗ The alternative is the state-based integration test
- A mock object is a fake object in your system that decides whether the unit test gas passed or failed. It does so by verifying whether the object under test called the fake object as expected. There is usually no more than one mock per test.
- A fake is a generic term that can be used to describe either a stub or a mock object (handwritten or otherwise), because they both look like the real object. Whether a fake is a stub or a mock depends on how it’s used in the current test. If it’s used to check an interaction (asserted against), it’s a mock object. Otherwise, it’s a stub.
Mock vs Stubs
- A stub can never fail a test
- A mock is used to verify whether or not the test failed
Using stub
Using mock
- Creating and using a mock object is much like using a stub, except that a mock will do a little more than a stub: it will save the history of communication, which will later be verified in the form of expectations.
- In other words, if the fake object has a state which is used on assertions, this is called mock. On the other hand, if the fake object doesn't have a state and is just used to replace a direct dependency and simulate a particular case, then is called stub.
Only one mock object per test that is testing only one thing. All the other fake objects should be stubs
Having more than one mock objects usually means that you are testing more than one thing.
- Overspecification is the act of specifying too many things that should happen that your test shouldn’t care about
- An isolation framework is a set of programmable APIs that makes creating fake objects much simpler, faster, and shorter than hand-coding them.
Running Tests
Tests runs as part of the automated build process
Test run locally by developers on their own machines
Steps
- Make a small change
- Run all tests to make sure that you haven't broken any existing functionality
- make sure that your code can still integrate well and not break any other project you depend on
- create a deliverable package and deploy
Mapping test classes to code under test
you should be able to do easily
Look at a project and find all tests that relate to it
Look at a class and find all tests that relate to it
Look at a method and find all tests that relate to it
Mapping
One test class for each class under test
Having separate classes for each complex method being tested
Test Utility classes and methods
Factory methods for objects
System initialization methods
Object configuration methods
Methods that setup or read from external resources such as databases
Special assert utility methods
Separate Unit from Integration tests, because the developers should run them as often as they need and should not be time consuming
Pillars of Good Unit Test
Trustworthiness
Maintainability
Readability
Test only one concept
Avoid test logic
A unit test should contain a series of method calls with assert but not control flows not even try-catch
Most probably test more than one thing
Decide when to remove or change a test
Production bug
Remove or refactor test
Test bug
Semantic or API changes
Eliminate duplicate tests
Check if the correct test added
1⃣ Comment out the production code
2⃣ Run all tests
3⃣ If nothing failed a test should
be added
4⃣ Write a failing test
5⃣ Uncomment the production code
and run again all the tests.
All should pass
Enforce test isolation
❌ Constrained tests order
❌ Hidden test calls
❌ Share state corruption
❌ Tests should not know the results of other tests
Use setup methods in a maintainable way
Remove duplication
Only the public contract is all that you need to care about
Avoid overspecification
private methods provide a hidden functionality and may change in the future which leads to breaking test
👉🏻 if a private method is worth testing it might worth to make it public or static
❌ assert purely the internal state
❌ multiple mocks or stubs
❌ assume specific order or exact string matches when it isn't required
Naming unit tests
1⃣ name of the method under test
2⃣ the scenario
3⃣ the expected behavior
Naming variables
Create good assert message
Separate actions from assert
avoid writing your own custom assert message
Components Priority to add tests
factors
logical complexity
dependency level
priority
rate each component for these factors 1(low) - 10(high)
create a graph (logic vs dependencies) on the rating and decide which components to exclude (by setting a logical threshold at 2 or 3) and from which component to start
(hard - first) Choose the more complex and harder to test
If team has experience
(easy - first) Choose the more complex but easier to test
If you are new in unit testing and need time to understand write tests
Design Guidelines for more testable code
make methods virtual by default (avoid final on methods)
use interface based desing
make class nonsealed by default (avoid final on classes)
Avoid instantiating concrete classes inside methods with logic
Get instances of classes for helper method factories IoC
Avoid direct calls to static methods
Prefer calls to instance methods that later calls statics
Avoid constructors and static constructors that do logic
Separate singleton logic from singleton holders