Why you should write code that is testable

That doesn’t mean you necessarily write tests for all the code you write (although in a professional setting most of your code probably should have tests).

Code that is testable tends to be simpler with less going on. If you can’t write a simple unit test for some functionality, it’s probably complicated. This makes it harder for people to understand the code. Also if you want to make modifications it’s not easy to verify it’s working correctly.

Some ways to make code more testable.

Avoiding side effects

Writing functions that don’t change anything tend to be easier to test. If it accepts some parameters and deterministically returns the same value every time, then it’s easier to verify the function works correctly.

E.g. let’s say we have a todo app and we want a function that counts recent (non-expired) todos.

def recent_todo_count(todos):                                                                        
    return len([todo for todo in todos if not todo.expired])

Controlling your code’s dependencies

If the functionality you are testing depends on something that’s not core to what you want to test, you can inject that functionality.

For example, let’s say you have a function that syncs a user’s google sheet with our todo app.

def sync_user_todos():                                                                                                                                                                                            
    google_sheet_client = get_google_sheet_client()                                                                                               
    rows = google_sheet_client.get_rows()                                                                                                         
    todos = transform_rows(rows)                                                                                                                  
    database.create(todos) 

The current function has three hard-coded dependencies: get_google_sheet_client, transform_rows, and database.

If get_google_sheet_client returns a real google client, you have to connect to an actual sheet to verify things are working. Similarly, if database is a hard-coded connection to a database, you need a real database running.

If you provide the google_sheet_client and the database to your function, you can control how those work.

def sync_user_todos(google_sheet_client, database):                                                  
    rows = google_sheet_client.get_rows()                                                            
    todos = transform_rows(rows)                                                                     
    database.create(todos)    

For example, in your test, you can define a fake google client object or mock that has a get_rows method and returns some specified data. Similarly for database, we only care that the create method was called with some expected input.

Finally, if transform_rows is very complex and the main thing we want to test is that we got rows from the client, called a method to transform them and then called a database, we can even inject the transform function as well.

class TestSync(unittest.TestCase):                                                                                                                                                                          
                                                                                                                                                                                                            
    def test_sync_user_todos(self):                                                                                                                                                                         
        google_sheet_client = Mock()                                                                                                                                                                        
        rows_from_client = ['rows']                                                                                                                                                                         
        google_sheet_client.get_rows.return_value = rows_from_client                                                                                                                                        
        database = Mock()                                                                                                                                                                                   
        transformed_rows_result = ['transformed']                                                                                                                                                           
        transform_rows = Mock(return_value=transformed_rows_result)                                                                                                                                         
                                                                                                                                                                                                            
        sync_user_todos(google_sheet_client, database, transform_rows)                                                                                                                                      
                                                                                                                                                                                                            
        transform_rows.assert_called_with(rows_from_client)                                                                                                                                                 
        database.create.assert_called_with(transformed_rows_result)                                                                                                                                         
                                                                                                                                                                                                            
if __name__ == '__main__':                                                                                                                                                                                  
    unittest.main()   

Leave a comment