Testing Python

21 Jul 2018

Why?

Though I’ve become accustomed to writing bash scripts to automate the testing of my command–line applications, upon being introduced to Ruby testing while reading Michael Hartl’s The Ruby on Rails Tutorial, I was surprised by how much easier debugging is when you’re able to write tests for each component of the program, rather than just the input/output of my bash scripts. So given my proclivity for Python, I instantly became curious about the process of implementing similar tests in Python.

Okay, How?

After a bit of research into the available testing frameworks, I decided on pytest due to the balance of capability and ease–of–use. Additionally, we’ll be writing a few tests for my yt2mp3 program, which I had previously been testing using one of the aforementioned bash scripts.

Setup

Before we start writing tests, we need to create a new file with the following convention, test_{filename}.py. This is so that pytest can accurately identify the files that contain tests to execute, when we run from the command–line, as shown below:

$ pytest

I prefer running pytest with the -v or --verbose flag, as this prints the test that is being run and whether it passed/failed.

Basic Testing

To get started, the first test we’re going to write is going to ensure that the program is able to accurately retrieve the title of a YouTube video when given a URL.
For this we’re going to:

  • Get the URL for a YouTube video with a known title
  • provide the URL to the yt2mp3.getVideoTitle() function
  • Check that the function returns the expected video title
import os, pytest, yt2mp3
# We're not using 'os' in this test but we will later

def test_video_title():
    url = 'https://www.youtube.com/watch?v=C0DPdy98e4c'
    title = yt2mp3.getVideoTitle(url)
    assert title == 'TEST VIDEO'

You’ll notice that the only difference in this function is that pytest uses an assert–statement where you might usually expect a return–statement.

Similarly, we can also write a test to check that the yt2mp3.getVideoList() function successfully retrieves the URLs for each video in the provided playlist. For this test, I’ve created a simple test playlist that features three videos to keep the number of URLs manageable and prevent modifications from changing the expected result.
So we need to:

  • Provide the URL for our test playlist
  • Provide a list of the URLs for each video in the playlist
  • Check that the function returns a list that matches the defined list
def test_get_playlist():
    url = 'https://www.youtube.com/playlist?list=PLGqB3S8f_uiLkCQziivGYI3zNtLJvfUWm'
    video_list = ['https://www.youtube.com/watch?v=_FrOQC-zEog','https://www.youtube.com/watch?v=yvPr9YV7-Xw','https://www.youtube.com/watch?v=-EzURpTF5c8']
    playlist = yt2mp3.getVideoList(url)
    assert playlist == video_list

Using Fixtures

To introduce the idea of fixtures, we’ll write a test that requires that we have a Song object, which stores the data necessary for downloading and setting the ID3 tags of the output mp3 file. Therefore, it’s understandable that a similar Song object may be necessary to test multiple functionalities of the program. That’s where fixtures come in.

@pytest.fixture
def test_song():
    data = yt2mp3.getSongData('Bold as Love', 'Jimi Hendrix')
    data['video_url'] = yt2mp3.getVideoURL(data['track_name'], data['artist_name'])
    yt2mp3.Song(data)

While, you can see that there’s not a lot of code that goes into creating the object, it’s best to follow the DRY principal and avoid redundancy.
Now that our fixture is defined, we are going to use it to write two tests to check the program’s ability to download a video and convert the video to an mp3.
To test the program’s download functionality, we’ll utilize the fact that the yt2mp3.download() function returns the filepath when the download is successful by asserting that the returned filepath exists.

Notice that we’ve provided our test_song fixture as a parameter of the test function.

def test_video_download(test_song):
    video_path = yt2mp3.download(test_song.video_url)
    assert os.path.exists(video_path)

Once the program passes the above test, we now know that we have a video in the ~/Downloads/Music/temp/ directory that we can use to test the program’s conversion to mp3. Additionally, since the yt2mp3.convertToMP3() function also returns the output filepath we are able to use a similar method for validation as we saw in the previous test, by checking the existance of the output path.

def test_convert_mp3(test_song):
    temp_dir = os.path.expanduser('~/Downloads/Music/temp/')
    video_path = os.path.join(temp_dir, os.listdir(temp_dir)[0])
    song_path = yt2mp3.convertToMP3(video_path, test_song)
    assert os.path.exists(song_path)

Though, the yt2mp3.convertToMP3() is also responsible for deleting the converted video file after the conversion. Luckily, we’re also able to validate this process within the same test by adding another condition and a bit of additional logging.

def test_convert_mp3(test_song):
    errors = []
    temp_dir = os.path.expanduser('~/Downloads/Music/temp/')
    video_path = os.path.join(temp_dir, os.listdir(temp_dir)[0])
    song_path = yt2mp3.convertToMP3(video_path, test_song)
    if os.path.exists(video_path):
        errors.append('The video file wasn\'t deleted after conversion')
    if not os.path.exists(song_path):
        errors.append('The output MP3 file doesn\'t exist')
    assert not errors, 'errors occured:\n{}'.format('\n'.join(errors))

Conclusion

Now we already have tests that cover a significant amount of the programs processes, without requiring a whole lot of work(or code). Hopefully, these examples have provided you a launchpad for testing your own python programs. Though I expect that I’ll be updating this post or posting a follow–up with more helpful info as I become increasingly versed in Python testing.

Back to Top

Brett Stevenson


Published

Share this post