Search code examples
pythonmockingpython-unittestgoogle-api-python-client

How to Mock a YouTube build object, search results of YouTube query


I'm trying to add unittesting to my python project and have gotten stuck trying to mock my YouTube build object. The variable I'm having trouble mocking is the results variable.

The Mock object is passed to the results variable in my app.py, but whenever the next line tries to run,

video_id = results['items'][0]['id']['videoId'],

I get this error in my terminal: video_id = results['items'][0]['id']['videoId'] TypeError: 'Mock' object is not subscriptable.

Solutions I've tried:

  1. @patch('app.youtube.search.list.execute') Error: AttributeError: 'function' object has no attribute 'list'

  2. mock_youtube_search.return_value.list.execute.return_value = response_data This doesn't return any errors and renders the page, but the video_id variable is all wrong. The video_id variable appears at the end of the embed URL, https://www.youtube.com/embed/. Below, I've shown what the video_id variable's value is in the <iframe> tag.

This is the <iframe> tag that shows when I print the html using solution 2 that I tried:

<iframe src="https://www.youtube.com/embed/<MagicMock name=\'search().list().execute().__getitem__().__getitem__().__getitem__().__getitem__()\' id=\'2662986714704\'>"></iframe>

This is my POST request for my index route in my app.py file.

    elif request.method == "POST":
        # Get movie info from movie_id that was submitted in the form
        movie = db.execute("SELECT * FROM movies WHERE id = ?", request.form.get("movie_id"))[0]

        # Search youtube build object and get back a list, passing in movie title the user clicked and set number of results = 1, set parameter part to snippet
        results = youtube.search().list(part='snippet', q=f'{movie["title"]} trailer', maxResults=1).execute()

        # Find video ID of the results query
        video_id = results['items'][0]['id']['videoId']

        # Now that we have YouTube video ID, use embed URL to link to the video in the iFrame tag
        movie_link = f'https://www.youtube.com/embed/{video_id}'

        return render_template("description.html", movie = movie, URL = large_poster_path_URL, movie_link = movie_link)

Here is the code in my app_test.py file.

    @patch('app.youtube.search')
    @patch('app.db.execute')
    def test_index_route_POST(self, mock_db_execute, mock_youtube_search):
        # Make sure user is logged in
        with self.app.session_transaction() as session:
            session['user_id'] = 1
            session['logged_in'] = True

        # Mock database SELECT query
        mock_db_execute.return_value = [
            {'id': 2, 
             'title': 'Teenage Mutant Ninja Turtles: Mutant Mayhem', 
             'overview': "After years of being sheltered from the human world, the Turtle brothers set out to win the hearts of New Yorkers and be accepted as normal teenagers through heroic acts. Their new friend April O'Neil helps them take on a mysterious crime syndicate, but they soon get in over their heads when an army of mutants is unleashed upon them.", 
             'release_date': '2023-07-31', 
             'poster_path': '/poster_path_3.jpg'}
        ]

        # Mock the YouTube API response using custom mock
        response_data = Mock(return_value={"items": [{"id": {"kind":"youtube#video","videoId":"IHvzw4Ibuho"}}]})
        print("This is the response data: ", response_data())
        mock_youtube_search.return_value = response_data

        # POST request to index route
        response = self.app.post('/', follow_redirects=True)
        print(response.data) # Debugging
       
        # Assert that the response status code is 200 (OK)
        self.assertEqual(response.status_code, 200)

        # Assert that index.html is showing correct database information
        soup = BeautifulSoup(response.data, 'html.parser')
        movie_title = soup.find('p')
        movie_title_text = movie_title.text.strip()
        self.assertEqual(movie_title_text, 'Movie title: Teenage Mutant Ninja Turtles: Mutant Mayhem')
        

Where am I messing up?


Solution

  • TLDR; this is my solution: Replace mock_youtube_search.return_value = response_data with mock_youtube_search().list.return_value.execute= response_data

    I figured it out! The YouTube build object from the google-python-api-client is weird to figure out. So, my Mock() data was correct and I had it in the right format. The issue stemmed from not placing that return value: {"items": [{"id": {"kind":"youtube#video","videoId":"IHvzw4Ibuho"}}]} in the correct location whenever I return it.

    Look at this line of code, mock_youtube_search.return_value = response_data, and this patch decorator, @patch('app.youtube.search'), in my app_test.py snippet from my post above.

    I'm mocking the results variable in my app.py:

    results = youtube.search().list(part='snippet', q=f'{movie["title"]} trailer', maxResults=1).execute()

    In essense, I was making the return value for just the youtube.search() part, not the entire statement. I want get a mock return value for the execute() portion, which is a nested call. So, I had to play around and use some logic to get the correct sequence of .return_value and where to place it. Since I patched the youtube.search() part, I had to call the mock_youtube_search like so, mock_youtube_search(). Then, I had to add this to the end of it, .list.return_value.execute.

    Summary: call the mock object that corresponds to the patch decorator, add .list.return_value since .list() returns a value in the YouTube build object, then add .execute since that's what you want to actually mock, set that entire statement combined, mock_youtube_search().list.return_value.execute equal to the response_data.

    I printed the data out so you can see it printed correctly.Solution Output

    Original Output. Incorrect Output