Dipping my toes in Test-Driven Development with Python

Featured on Hashnode

Intro

When you start learning you create an application, maybe even deploy it on some free server, but that's it. There is no unsatisfied customer that found some error, there is no client that changed their requirements 3 hours after you pushed to production. There is also no code maintenance. In reality, software is a living thing.

In a real-life, you'll keep changing your code, adding new requirements, fixing bugs. And that can get messy after a while.

There are some practices that make it easier for you. One of those is Test Driven Development.

Requirements

Since this is not a tutorial, you don't really need any knowledge to read it. However, I assume that you know enough Python to be able to follow.

What is Test Driven Development

Test-Driven Development with the abbreviation TDD is a methodology in software development where you write a test for function before it is implemented.

Writing TDD means writing in a cycle:

  1. RED - write failing test
  2. GREEN - write the simplest piece of code that will get the test to pass
  3. REFACTOR - improve the code while keeping the test pass

Although it seems that it takes a longer time to develop that way, it saves you a lot of time after you've mastered it. Similarly, it costs you a lot of nerves when you start learning it but it saves you a lot of nerves in a long run.

Palindrome example

I wrote a simple function, that checks if a string is a palindrome. The only palindrome I know on the top of my head is the word "tat" (it means a thief in my language).

A word is very simple to check: you just have to revert it. If words are the same, you know that is a palindrome. You don't naturally think about 10 other cases in which that maybe wouldn't work. It works and that's enough.

But, because I was practicing TDD, I had more cases that got run. It turned out that my function didn't work for sentences. Sentences included capital letters, spaces, and punctuations. Because of TDD, I quickly figured out I need to remove special characters and spaces.

That mistake is easy to catch, but when your code is more complex, it can take a lot of time to figure out that something is not okay and how to fix that.

What I was doing

For my first project, I selected something simple. If I would be developing something complex, my focus would be on the development of the program instead of on learning TDD.

I was building an expense tracker where the user can enter an amount, date, and description of expense via CLI which is stored in the database. It's also possible for the user to list existing expenses.

To build it I used:

  • Python
  • pytest
  • TXT file as a database

Given, when and then

Given, when, then is a structure that you can use to describe the expected behavior of the unit under test. In Python you can write it as a docstring:

"""
GIVEN:  initial conditions for the test
WHEN:   what is occurring that needs to be tested?
THEN:   expected outcome
"""

I always wrote that docstring first and use it as a guideline to write the test. It's also easier to see what's going on for someone who sees the test for the first time.

My first test

First, I installed pytest. It's a Python library for writing automated tests. I started with a unit test. The unit test is a test that tests a small part of the program in isolation of other parts. To check whether those units work together correctly you use integration and end-to-end tests. When I started writing the test for my first function there was no code yet. At this point, function user_input_divide, existed only in my head. So I wrote a test for it:

def test_user_input_divide(): #1
    #2
    """
    GIVEN user input
    WHEN user_input_divide is called
    THEN input is divided into 3 pieces (amount, date, description)
    """

    raw_input = "33.22;2020-11-05;This is a description" #3
    amount, date, description = user_input_divide(raw_input) #4

    #5
    assert amount == "33.22"
    assert date == "2020-11-05"
    assert description == "This is a description"
  1. name of the test function must be prepended with test_ - pytest automatically discover and runs functions with test_ prefix

  2. Given, when, then formula:

    • Given a user input -in this function I do not care how will I get user input, I just assume it exists and it's in the correct form.
    • the function user_input_divide is going to be called user_input_divide
    • as a result user's input must be divided into 3 pieces - amount, date, description
  3. valid user's input for the function

  4. function user_input_divide is called to divide the input - the function user_input_divide doesn't exist yet.
  5. assert is a standard Python statement that can be used with pytest. On the left side, you provide the actual outcome of the function and on the right side, you provide the expected outcome. == in the middle means that you expect that both sides will be equal

After that, I ran the test with the command:

pytest

The test, of course, fails.

Once I had a failing test I implemented a function with a hardcoded result.

def user_input_divide(raw_input):
    return ["33.22", "2020-11-05", "This is a description"]

I run the test again and it failed - again.

Why? Let's read the error:

NameError: name 'user_input_divide' is not defined

Of course, I haven't imported the function into my test module yet.

That should fix it:

from expense.user_input import three_part_input, user_input_divide

def test_user_input_divide(): #1
    ...

I run the test again, it passed.

But that didn't cut it. That function didn't do anything, I just knew that everything is connected the right way.

I changed the function from hardcoded to real implementation:

def user_input_divide(raw_input):
    return raw_input.split(";")

I run the test and it passed.

Where to next?

Imagine TDD like building something with Legos. First, you create all the lego blocks, and only after that, you put them together. Following the expense tracker example, it would go something like this:

  • Splitting user input into 3 separated parts - DONE
  • Checking if all the data is in a correct form (date in form of YYYY-mm-dd, the amount in a form of a float)
  • Currently, all the data is in a string - you need to transform the amount into a float and date into a datetime object
  • Put date into the object
  • Persist the object to a database

For each of these, you first need to write a test. Only after that, you put all that together and test if it works correctly altogether.

Conclusion

This was not meant to teach you test-driven development but to introduce it to you. I hope I sparked a little interest in TDD. It's a great way towards easier development, maintenance, and upgrading. I recently started learning TDD, so you can follow my journey here. I'm also planning to create beginner-friendly tutorials in the future.

Photo by Samuele Errico Piccarini on Unsplash

Interested in reading more such articles from GirlThatLovesToCode?

Support the author by donating an amount of your choice.

Martin Smith's photo

Great Article!

Can you elaborate how you can integrate these tests in on CI/CD?

GirlThatLovesToCode's photo

Thank you. As I wrote in my post, I only just started learning. But you can check this article, you have a config written there.

Justin Frederick's photo

Thank you for sharing your journey into your new learning format. I am a student learning computer software engineering and, I am no where near ready to fully grasp every concept you mentioned here but, I did follow for the most part and I am happy to see that there is a different approach to coding.

GirlThatLovesToCode's photo

I'm glad to hear that, that was the purpose of my blog. I'm nowhere near ready to fully grasp every concept here either, so don't worry.

That comes only with a lot of practice.

Diana Chin's photo

Great article!