Why?

Though I’ve become accustomed to writing bash scripts to automate the testing of my commandline applications, upon being introduced to Ruby testing while reading Michael Hartls The Ruby on Rails Tutorial, I was surprised by how much easier debugging is when youre 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 easeofuse. Additionally, well 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 commandline, as shown below:

1$ 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 were 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 were 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
1import os, pytest, yt2mp3
2# We're not using 'os' in this test but we will later
3
4def test_video_title():
5 url = 'https://www.youtube.com/watch?v=C0DPdy98e4c'
6 title = yt2mp3.getVideoTitle(url)
7 assert title == 'TEST VIDEO'

Youll notice that the only difference in this function is that pytest uses an assertstatement where you might usually expect a returnstatement.

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, Ive 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
1def test_get_playlist():
2 url = 'https://www.youtube.com/playlist?list=PLGqB3S8f_uiLkCQziivGYI3zNtLJvfUWm'
3 video_list = [
4 'https://www.youtube.com/watch?v=_FrOQC-zEog',
5 'https://www.youtube.com/watch?v=yvPr9YV7-Xw',
6 'https://www.youtube.com/watch?v=-EzURpTF5c8'
7 ]
8 playlist = yt2mp3.getVideoList(url)
9 assert playlist == video_list

Using Fixtures

To introduce the idea of fixtures, well 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, its understandable that a similar Song object may be necessary to test multiple functionalities of the program. Thats where fixtures come in.

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

While, you can see that theres not a lot of code that goes into creating the object, its 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 programs ability to download a video and convert the video to an mp3.
To test the programs download functionality, well 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.

1def test_video_download(test_song):
2 video_path = yt2mp3.download(test_song.video_url)
3 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.

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

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

1def test_convert_mp3(test_song):
2 errors = []
3 temp_dir = os.path.expanduser('~/Downloads/Music/temp/')
4 video_path = os.path.join(temp_dir, os.listdir(temp_dir)[0])
5 song_path = yt2mp3.convertToMP3(video_path, test_song)
6 # highlight-start
7 if os.path.exists(video_path):
8 errors.append('The video file wasn\'t deleted after conversion')
9 # highlight-end
10 if not os.path.exists(song_path):
11 errors.append('The output MP3 file doesn\'t exist')
12 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 Ill be updating this post or posting a followup with more helpful info as I become increasingly versed in Python testing.