Clean Code TDD Principles

Notes about Test Driven Delevopment & Clean Coding


Last Updated: May 05, 2021 by Pepe Sandoval



Want to show support?

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 star

TDD Clean Code Principles

TDD Intro

  • 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

  • The only way to go fast is to keep the code clean if you want to go slow let the code get dirty

  • 3 laws of TDD review:

    1. Test first You are not allowed to write any production code until you have a unit test failing first (Focus on finding a problem)
    2. Stop test to write production code Stop writing the test as soon as it fails and start writing code (just enough) to make it pass (Focus on solving a problem)
    3. Stop production code when test passes & refactor Stop writing production code as soon as the test passes and refactor code to keep it clean (Focus on cleaning up the mess we just made)
    • Repeat process
  • Single Assert rule/ Triple-A (Arrange-Act-Assert) Rule

    • In general we want that every unit test has one and only one assert but it is more important we arrange our test correctly
    • Every unit test should be broken up into 3 parts: Arrange-Act-Assert
      • Arrange: This part creates the data and context for the test (done in setup or partially in there)
      • Act Part to call the method/function to be tested
      • Assert part to verify code did what it was supposed to do, usually with a logical assert

Test Phases

  • Incremental Algorithmic Find a solution through a sequence of incremental generalization
# 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])
  • 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

Clean Tests

  • Phases of a test:

    1. Arrange:
      • Set the system into the state to run the test (this state is the test fixture)
      • This usually involves loading variables or data structures to feed the test
      • Every test usually does it's own arrange we only use setup when there is a group of tests that share a common arranging
    2. Act: Where we call the function or perform the action(s) we want to test
    3. Assert: When we actually check the output of the action to make sure it is what we expect
    4. Annihilate:
      • Where we undo everything done in the arrange and put the system back in the state before we tested
      • Sometimes every test need to do it's own little Annihilations the only annihilations we do in a teardown are shared annihilations
  • Types of Test Fixtures (State of a test):

    • Transient Fresh: Created from scratch and destroyed for every test
    • Persistent Fresh: Survives from test to test but initialized around every test (This means state was NOT destroyed just reinitialized)
    • Persistent Shared: Allows state to accumulate
# 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

Test Design

  • 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

    1. SRP for Tests: every test function and class should have one responsibility, this responsibility should be the one as the class being tested
    2. OCP for Tests We want to be able to change the production code without needing to change/update the tests or at least not changing it very much. This means we need to avoid coding our test is a way they know too much of the internals of the class they are testing, which may require some changes in the class from production code
    3. LSP for Tests: if we use polymorphism in our test they should conform with LSP (they clients of base class should be able to use the derivatives without knowing the difference)
    4. ISP for Tests: If the tests use interfaces that have methods they don't call, tests have too much knowledge
    5. DIP for Tests: Test code depends on production code, the production code never depends on the tests
  • 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

Test Naming

WE NEED TO DECOUPLE TESTS FROM IMPLEMENTATION DETAILS JUST LIKE CLEAN PRODUCTION CODE

Test Process

  • Techniques for writing tests

    1. Fake it until you make it:
      • We make a test case pass by faking the production code, then we make the next test pass by faking a little less, we continue this cycle each time faking it less and less until we are not faking it anymore
      • You make a test pass by first writing the wrong code but that actually make the test pass, code is not technically wrong it's just specific to make the test pass and not generally correct
      • Test a functionality from the outside-in, you first test and solve the simple peripheral issues like validating arguments or simple inputs then address the most complex inner workings
    2. Stairsteps Tests:
      • Write a test that will allow you to write the next test, then if you see the first test has no purpose you can delete it
      • Intermediate tests we write to be able to write the test we really want to keep
    3. Assert First:
      • This technique tells us to write the test backwards, starting from the assert
      • Start writing the assert you want to test then make the code that the assert needs (or in other words the code the assert wants to see)
    4. Triangulation:
      • It's a way to create generalization by writing two or more tests that will force you to generalize your production code
      • You first write a test that you can make it pass with a trivial/specific implementation then you write a test that forces you to write code that generalizes the implementation and make both tests pass
    5. One to Many:
      • The best way to deal with a collection of objects is to deal with one object first
      • To implement an operation that works with a collection of objects, implement it without the collection first then make it work with the collection
      • Make the test work in the singular case then make it work in the plural case
  • 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:

    • The best tests read as well written specifications/requirements of a system
    • Tests are the real requirements
    • When we write tests what we are writting are specifications so they should read like specifications
    • Write tests that explain themselves like specifications, Write the tests for an audience
    • Write the tests that'd want to read!
  • Test behavior NOT APIs Write your tests to express the behaviors you expected from the system

  • Rules of simple design for production code:

    1. Focus on Functionality: Code has to pass all the tests (First Make it work)
    2. Focus on Refactor: No duplication in the code nor bad practices (Then make it right)
    3. Focus on Internal Structure: Code should express every idea the author wants to express (Make sure to make it right)
    4. Focus on Optimization: Minimize number of classes and methods (Make it small and fast)
  • 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

Mocking

  • 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:

    • The Dummy:
      • Object that implements and interface where all the methods do nothing and returns null/None or a value close to zero
      • Object that does nothing and when it has to return something it returns as close to nothing as possible
      • Usually used when we are testing a method that takes an argument and neither the test nor the method cares what happens to that argument (we create a dummy object and pass it to this argument)
    • The Stub:
      • Object that does nothing but returns a value that is consistent with the needs of the test (usually somethin not a null/None nor zero)
      • Does nothing but returns special fixed values the test need to perform its desired testing
      • Useful to drive the code through a certain execution pathway
    • The Spy:
      • Object that does nothing, returns the values expected by the test and remembers facts about how it was called so that it can be reported to the test later
      • Objects to remembers if a function is called, if it was called with the right arguments, if it was called the right amount of times, etc
      • Useful to check we call external services
    • True Mock:
      • Object that does nothing, returns values useful to the test, remembers facts about how it was called and it knows what should happen
      • Knows what to check so the test asks the true mock if everything went as expected
    • The Fake:
      • It's a simulator that respond different to different inputs more in a way real objects would behave
      • Object that pretends to have a real behavior

Test Doubles Classification

  • TDD schools of thought:
    • Mockism (London School)
      • Puts emphasis on the use of spies to verify the implementation of an algorithm
      • Tolerate higher coupling of tests and production code in return for increased assurance that the code really does what we expect
      • Useful when we test external components (when we cross DIP boundaries of the system)
    • Statism (Chicago/Detroit/Cleveland School)
      • Puts emphasis on the values returned by the functions
      • Prefers to decouple the tests from the implementation of the algorithms
      • Useful when we want to reduce the coupling of tests to the implementation

Mocking/Testing Patterns:

  • Test Specific Subclass:
    • You want to test a function in class but you want to modify the behavior of other functions in that class (the one you are testing)
    • Override the right methods of a class to force a behavior you want to test
    • A good way to make testable classes is to write your classes in a way their methods can be easily overridden by tests specific subclasses

Test Specific Subclass Example

  • Self-Shunt: The test itself or other class becomes the that implements the service interface that is used by the class to be tested

Self-Shunt Example

  • Humble Object
    • Used when you have a class that has a code that is hard to test or interacts with IO
    • You first separate the code that is hard to test or that interacts with HW in an interface, then have a class that uses that interface and has other algorithm(s) you want to test, you can have helper functions that would be the ones that actually call/use the interface that is hard to test so that you can overridden them in your test
    • Isolate the testable code from the code that is hard to test you can override the hard to test code
    • Untestable code must depend on testable code
    • Useful at the IO boundaries (GUIS, DB)

Humble ObjectExample

Transformation Priority Premise

  • 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:

    1. None/null: Represents starting point of function, go from nothing to function that does nothing
    2. None/null to constant: Transforms null/none to a static constant, function that does nothing to a function that returns something constant
    3. Constant to variable: Transforms a static constant to a variable or an argument of the function, usually variable will hold value of constant
    4. Add Computation: Add one or two simple computations (math) or init a variable
    5. Split flow: Add if statement to split flow of execution of a function into two paths
    6. Variable to Array: Change from one of something and you want more of the same thing
    7. Array to Container: Use it when we want to generalize an array into something more comprehensive like a dict or set
    8. If to While: Use it when we release that a flow that has been split must also be repeated, commonly used after a variable to array transformation
    9. Recurse: Use it when we have a function and we want to repeat its execution (we make the function call itself)
    10. Iterate: Use when we want to repeat an operation
    11. Assign: use it when we want to alter the state of a previously existing variable (not init but reassign new value to previously existing variable)
    12. Add case: Use it when we have a split flow more than once (add else-if/elif or add a case to switch statement)
  • Transformation Priority Premise

    • Try to chose the transformation with the highest priority
    • When you get to a fork in road (when there's more than one way to make a test pass) make it pass with the highest priority transformation, then possibly the algorithm will be better

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)

References

Want to show support?

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...