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:
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 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
1import os, pytest, yt2mp32# We're not using 'os' in this test but we will later34def test_video_title():5 url = 'https://www.youtube.com/watch?v=C0DPdy98e4c'6 title = yt2mp3.getVideoTitle(url)7 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
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, 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.
1@pytest.fixture2def 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 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.
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, we’re 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-start7 if os.path.exists(video_path):8 errors.append('The video file wasn\'t deleted after conversion')9 # highlight-end10 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 I’ll be updating this post or posting a follow–up with more helpful info as I become increasingly versed in Python testing.