Notes about Test Driven Delevopment & Clean Coding
Last Updated: May 05, 2021 by Pepe Sandoval
If you find the information in this page useful and want to show your support, you can make a donation
Use PayPal
This will help me create more stuff and fix the existent content...
All the elements we are composed of were made when a giant star blew itself to pieces, nothing else that we know of can cram this many protons into a nucleus We are all made out of the ashes of dead stars
Disciplines are arbitrary or have arbitrary components and behaviors that are driven by a substantial motive. Arbitrary means in simple words it is just there because of a guess that has proven to work based on experience and it doesn't cause problems
We can't clean code until we eliminate the fear caused by changing and breaking the code
Tests eliminate fear so they let you clean code
The only way to go fast is to keep the code clean if you want to go slow let the code rot and get dirty
If you want a flexible system, get a suite of tests that you trust
A test suite that doesn't give you enough confidence to know your code works is useless
Giving priority to tests, makes a system testable (and also decoupled)
Inheritance is the tightest coupling we have so we want to have as little inheritance as possible or inherit as few implemented things as possible
The tests must be like code examples of how to use your system/app, they are short snippets of the code that explain how one part of the system works.
Test Driven Development Examples:
Every creative effort on the planet is done iteratively, we don't write perfect code the first time. It requires refinement and rework
If a single change in production code breaks the tests there is something wrong with the tests, they are too coupled to production code
As the tests get more specific the code gets more generic
Let the test drive the solution, instead of imposing the solution on the tests
Incremental Algorithmic Find a solution through a sequence of incremental generalization
Getting Stuck Means there's nothing incremental you can do to pass the currently failing test, this means you have to write a lot of production code or different modules to make the test pass
To avoid getting stuck it is very important to start by writing the most simple test cases we can then gradually climb the ladder of complexity and make the test pass by generalizing the production code instead of doing specific fix just to get the test to pass
Mutation testing: is an computationally expensive technique of testing in which a test suite that pass is run and the coverage is recorded then a mutation tool changes/mutates something in the code (change a ==
for a !=
for example`), then runs the test suite again, measures the coverage expecting one or more test failures, if a test pass it means a mutation is alive, these mutation are counted and reported since they point we have either a bad test, a bad code or bad coverage
Single Assert rule/ Triple-A (Arrange-Act-Assert) Rule
Test and Test code are just as important as production code
Try to get 100% coverage even knowing it is not possible
GUIs are separated in two layers, presenter and view
# python3 -m unittest test_prime_factors.py from unittest import TestCase import math def return_list_of_prime_factors(n): prime_factors = [] divisor = 2 #while (divisor <= math.sqrt(n)): # Change while loop to improve performance but need to add code for 2 test case ->if n > 1: prime_factors.append(n) while (n > 1): while (n % divisor == 0): prime_factors.append(divisor) n //= divisor divisor += 1 return prime_factors class TestPrimeFactors(TestCase): def assert_prime_factors(self, n, expected_list_of_primes): self.assertEqual(expected_list_of_primes, return_list_of_prime_factors(n)) def test_prime_factors(self): self.assert_prime_factors(1, []) self.assert_prime_factors(2, [2]) self.assert_prime_factors(3, [3]) self.assert_prime_factors(4, [2, 2]) self.assert_prime_factors(5, [5]) self.assert_prime_factors(6, [2, 3]) self.assert_prime_factors(7, [7]) self.assert_prime_factors(8, [2, 2, 2]) self.assert_prime_factors(9, [3, 3]) self.assert_prime_factors(2*2*3*3*5*7*11*11*13, [2,2,3,3,5,7,11,11,13])
Phases of a test:
setup
when there is a group of tests that share a common arrangingteardown
are shared annihilationsTypes of Test Fixtures (State of a test):
# python3 -m unittest test_fixtures.py from unittest import TestCase class TransientFreshTest(TestCase): def __init__(self, *args, **kwargs): print('Construct TransientFresh') super(TransientFreshTest, self).__init__(*args, **kwargs) def setUp(self): print("setup TransientFresh") def test_1(self): print("test_1 TransientFresh") def test_2(self): print("test_2 TransientFresh") class PersistentFreshTest(TestCase): def __init__(self, *args, **kwargs): print('Construct PersistentFresh') super(PersistentFreshTest, self).__init__(*args, **kwargs) def setUp(self): print("setup PersistentFresh") def tearDown(self): print("teardown PersistentFresh") def test_1(self): print("test_1 PersistentFresh") def test_2(self): print("test_2 PersistentFresh") class PersistentSharedTest(TestCase): def __init__(self, *args, **kwargs): print('Construct PersistentSharedTest') super(PersistentSharedTest, self).__init__(*args, **kwargs) def setUp(self): print("setup PersistentSharedTest") def tearDown(self): print("teardown PersistentSharedTest") @classmethod def setUpClass(cls): print("Suite Setup PersistentSharedTest") @classmethod def tearDownClass(cls): print("Suite Teardown PersistentSharedTest") def test_1(self): print("test_1 PersistentSharedTest") def test_2(self): print("test_2 PersistentSharedTest")
To avoid large Setup method we can group test into hierarchies so a test function executes only the teardowns and setups it needs, we can use separate methods or inheritance
Try to compose actions, in general if you have a series of actions in a test it may mean you need an utility method that compose those actions into a single action
A test must be a Boolean operation but the number of physical assertion is irrelevant as long as they are composed of a single logical assertion which can be composed into a method to make clearer assertion
Usually one test file per class that are used, this means interfaces or inner classes don't have their own test file but in general we want to test behaviors not just classes
SOLID for tests: if we don't invest time in tests design we can end up with fragile tests, meaning a simple change in one test or adding a feature in production code causes a lot of tests to fail causing we need to go a change/tweak those changes
We usually don't test private functions because testing the public functions should be testing the private ones and also explicitly testing private functions will let test see private implementations that is too much knowledge for the tests
Testing triumphs encapsulation if you have to test private functionality you may need to promote it to public, protected or something else
Write the test that will force you to write the code you already decided you want to write
You can plan your development by writing a list of tests that need to be passed and while you write code and tests you will think of other tests to add to your list
It is a good practice to name our setups as the Given
and name the test functions for the action and assertions (the When
and Then
)
We don't want the code in our tests to be an endless stream of magic numbers and constants that can only be interpreted with perfect knowledge of the requirements we want the test to reminds us of what the requirements are this means names of test should reflect the requirements not the implementation
WE NEED TO DECOUPLE TESTS FROM IMPLEMENTATION DETAILS JUST LIKE CLEAN PRODUCTION CODE
Techniques for writing tests
REFACTOR YOUR TESTS Tests are part of your system so we must make an effort to keep them clean. If the tests are well designed we can recreate the production code from the tests (and the recreation can even be better) but even if the production code is well design we may not be able to recreate all the tests from the production code
Tests enable refactor and code reorg by eliminating the fear of change
Tests are Specifications:
Test behavior NOT APIs Write your tests to express the behaviors you expected from the system
Rules of simple design for production code:
Tests first For tests rules of simple design need to be reordered, we need to make the test express themselves first (assert first), then we make sure they pass finally we refactor and optimize if needed because tests come first, come first when we write them, when we refactor them, when we clean them and maintain them
Test Mocks / Test Doubles
What allows us to do mocking is a decoupled architecture, code must be decoupled from IO devices (web, databases, HW, etc) so we can provide different implementations
Every test can use different stubs that does just what a test needs
Test Doubles Classification:
Refactoring is a change in structure without significant change in resulting behavior: It is a change to the internal structure of the software in order to make it easy to understand, cheaper to maintain and without changing the observable behavior
Transformations are a change in behavior without a significant change in structure They are the counter part of refactor, they are small changes to the code that changes behavior but preserve structure
When a test is failing you strive to change behavior while preserving structure (without changing the structure too much), once the test passes you strive to improve structure without changing behavior
Transformations are small changes to the code that attempts to generalize the behavior without changing structure
The Transformation List:
Transformation Priority Premise
Duplicate code is always specific never general!
# python3 -m unittest test_sort.py from unittest import TestCase def sort(to_sort_list): sorted_list = [] size = len(to_sort_list) if size == 0: return to_sort_list else: lower_half = [] m = to_sort_list[0] higher_half = [] for i in to_sort_list[1:]: if i > m: higher_half.append(i) else: lower_half.append(i) sorted_list += sort(lower_half) sorted_list.append(m) sorted_list += sort(higher_half) return sorted_list class SortingTest(TestCase): def assert_sorted(self, unsorted_list, sorted_list): self.assertEqual(sort(unsorted_list), sorted_list) def sort_big_list(self, n): import random unsorted = [] for i in range(n): unsorted.append(int(random.random() * 10000.0)) my_sorted = sort(unsorted) for i in range(n-1): self.assertTrue(my_sorted[i] <= my_sorted[i+1]) # print(unsorted) ; print(my_sorted) def test_sortings(self): self.assert_sorted([], []) self.assert_sorted([1], [1]) self.assert_sorted([2, 1], [1, 2]) self.assert_sorted([2, 1, 3], [1, 2, 3]) self.assert_sorted([2, 3, 1], [1, 2, 3]) self.assert_sorted([3, 2, 1], [1, 2, 3]) self.assert_sorted([1, 3, 2], [1, 2, 3]) self.assert_sorted([3, 2, 2, 1], [1, 2, 2, 3]) self.sort_big_list(50000)
If you find the information in this page useful and want to show your support, you can make a donation
Use PayPal
This will help me create more stuff and fix the existent content...