- Laws:
- Dont write any PROD code until you have a failing unit test
- Stop writing the test as soon as it fails (including compiling errors)
- Stop writing PROD code as soon as the test passes
- Cycle:
- Red
- Green
- Refactor
- Write degenerate tests first
- Dont go too complex to fast
- Single assert (triple A) rule
- Single logic assertion
- OK: arrange, act, assert, assert, assert
- NOT OK: arrange, act, assert, arrange, act, assert, arrange, act, assert
- As the test gets more especific, the code gets more generic
- If you get stuck, most likely it was because:
- The above rule was broken
- Didnt write the degenerate tests firsts
- There are 4 phases
- Arrange
- Fixtures:
- Transient fresh
- Created and destroyed on every test
- No need of tear down, nothing survived
- Persistent fresh
- Survives from test to test
- Needs tear down function to remove persistance and make them fresh so it is ready for the next tests
- Persistent shared
- Initalized on every test but allows some state to be accumulated from test to test
- Needs suite tear down
- They are not mutually exclusive
- The freshier the better and transient is tops
- Transient fresh
- Fixtures:
- Act
- 2 or more actions function means there is some state sharing, meaning is not writen in a functional style
- In some cases where multiple actions is requiered due the nature of the test, multiple actions can be composed, this can be useful to keep actions clean
- Assertion
- Is a boolean expression
- One logical assertion which can be composed by many assertions (composed assert)
- In the composed assert the action can be also hidden, this can be useful
- Annihilation
- Undo everything was done on arrange
- Arrange
- Setup method: Common arrangements among group of tests
- Tear down: Common annihilations among group of tests
- Test hierarchy
- Nested setup methods
- This helps to avoid having long setup methods
- Compose a sequence of test into a single test
- This can be controversial
- This is a specual use case
- Place test under test directory
- Ideally one test file per class
- Tests is also software hence also needs design, same rules apply (SOLID, etc)
- Otherwise you will end up with fragile tests
- No need to test privates since public interface is tested, which also includes privates
- If testing privates is needed, you will have to promote the access to public or something similar
- Naming:
- Using
Given
,When
andThen
aproach is useful (does not always applies though) - Avoid using magic numbers and that kind of stuff on tests
- The idea is to be able to understand the tests without a perfect knowledge of the requierements
- Test can remind you of the requierements
- Using
- Fake it until you make it
- It is an incremental process until the poing where faking it is not needed
- Simpler tests first, leave to the end the complicated ones
- Some tests will allow you write the next tests, and after you can delete the previous ones
- Assert first
- Write the test backwards by starting with the assertion
- It force us to deal with errors, compile and execution
- Triangulation
- A way to create a more general solution
- By untrivializing implementation makes test pass
One to many
technique:- When dealing with collections of thing, test one thing first
- Make it pass first in the singular case and then the plural case
- Refactoring tests
- Refactoring tests is important
- Test are part of the system, not extra things, not inferior, not disposable, they are integral part of the system which cannot live without for very long
- Treat them with same care and discipline or even more, than the PROD code
- They are the key to enable refactor
The two disks
story:- PROD code is in one disk and test code is in another one. If one breaks with no backup, which one would you preffer to be?
- Creating PROD code from tests is easier and better than creating tests from PROD code
- Tests are specification
- Tests are specification so they should read like specifications
- The best test reads like well written specification
Write the test that you would want to read
- Test behavior not APIs: write what behavior do you expect independent of the API
- In all things the tests comes firsts:
- When writing them
- When refactoring them
- When cleaning and maintaining them
- Test doubles
- There is no such a thing as a test double
- Dummy: functions performs no actions and return as nothing as possible
- Stub: Is a Dummy + returns a value consistent with the needs of the tests
- Spy: Is a Stub + records facts about its invokations (if something was called, params sent, etc)
- True Mock: Is a Spy + it knows what should happen, the tests ask the mock if everything went as expected
- Fake: not Mock, not Spy, not Stub, not Dummy, is entire different, it is a simulator
- Has lot of logic
- Pretends to have realistic behavior
- They can grow thus they get more complicated
- Usually for unit tests stubs and spies are enough, not need of fake
- For integration tests fakes can be very useful
- Avoid fakes whenever you can
- Behavior vs State (Uncertainty Principle)
- Mockist (London school):
- Use of spies which are useful when testing things that crosses boundaries
- Tolerates higher coupling because the increased assurance
- Statist (Chicago/Detroit/Cleveland school)
- Emphasis on values returned
- Preffers to decouple tests
- Better when you are not testing something that crosses a boundary
- Mockist (London school):
- Mocking patterns
- Test specific subclass
- When testing a function in a class but you want to modify the behavior of other functions in that class
- Override functions so they do nothing
- Kind of a
stub
andspy
- Self-shunt
- Test class implements interface needed on the tests, the test class would need to be injected
- Kind of a
stub
andspy
- Humble object
- Isolate testable code from code that is hard to test (extract hard to test code)
- Code hard to test is the humble object
- This way untestable code depends on testable code
- This can be useful to test IOs, GUIs, etc
- Test specific subclass
- Mocking frameworks
- Usually not needed since mocks are easy to write
- Useful when overide sealed interfaces (like private stuff) is needed
- However on a well designed system it shouldnt be needed
- Useful on legacy environments for example
- Refactor: Changes in structure without significant changes on behavior
- Transformation
- Changes on behavior without significant changes on structure
- Small changes to the code that generalize behavior
- The Transformation list (first approximation, possible listed by priority)
- Null: Initial starting point of any function, function that does nothing, returns null or zero
- Null to constant: function that returns null to return something not null
- Constant to variable: change a constant into variable or argument
- Add computation: add one or two simple computations but cannot change state of any existing variables
- Split flow: split flow of execution into only two parts
- Variable to array
- Array to container: generalize an array into something more comprehensive like dict, set, stack, queue, etc
- If to while: splitted flow needs to be repeated
- Recurse
- Iteration: operation needs to be repeated but not by using recursivity
- Assign: Change the state of a variable but not when initializing first state
- Add case: When we have a splitted flow we want to split further
- *Duplicated code is always specific, never general
- The Transformation list is also arranged in complexity order
- High priority: simple and very low impact on the code
- Low priority: complicated and high impact on the code
- The premise: When there is a fork on the road, make the test pass with the highest possible priority transformation
- Choossing low priority transformation leads to a bad algorith
- Choossing high priority transformation leads to a good algorith
- Based on this we can conclude that when getting stuck is when having to pass a test case using a low priority transformation. Thus to avoid this, find a different test case which can be passed with higher priority transformation.