Search code examples
pythonunit-testingtestingpython-requestspython-unittest

How to mock requests.Session.get using unittest module?


I want to mock app.requests.Session.get from test_app.py to return a mocked requests.Response object with a 404 status_code to generate an InvalidPlayerIdException.

However, no exception is raised as seen from the below output. Is it because I'm using with clauses, or why doesn't it work?

Reference: https://www.pythontutorial.net/python-unit-testing/python-mock-requests/

Output:

(supersoccer-showdown) ➜  supersoccer-showdown-copy git:(main) ✗ python -m unittest                                                                                               git:(main|…3 
F
======================================================================
FAIL: test_pokemon_player_requestor_raise_exception (test_app.TestPlayerRequestor.test_pokemon_player_requestor_raise_exception)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/opt/homebrew/Cellar/python@3.11/3.11.2_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/mock.py", line 1369, in patched
    return func(*newargs, **newkeywargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dkNiLyIv/supersoccer-showdown-copy/test_app.py", line 21, in test_pokemon_player_requestor_raise_exception
    self.assertRaises(InvalidPlayerIdException, requestor.getPlayerById, 1)
AssertionError: InvalidPlayerIdException not raised by getPlayerById

app.py:

from __future__ import annotations
import abc
import requests

class InvalidPlayerIdException(Exception):
  pass

class Player(abc.ABC):
  def __init__(self, id: int, name: str, weight: float, height: float) -> None:
    self.id = id
    self.name = name
    self.weight = weight
    self.height = height

class PokemonPlayer(Player):
  def __init__(self, id: int, name: str, weight: float, height: float) -> None:
    super().__init__(id, name, weight, height)    

  def __repr__(self) -> str:
    return f'Pokemon(id={self.id},name={self.name},weight={self.weight},height={self.height})'

class PlayerRequestor(abc.ABC):
  def __init__(self, url: str) -> None:
    self.url = url

  @abc.abstractmethod
  def getPlayerCount(self) -> int:
    pass

  @abc.abstractmethod
  def getPlayerById(self, id: int) -> Player:
    pass

class PokemonPlayerRequestor(PlayerRequestor):
  def __init__(self, url: str) -> None:
    super().__init__(url)

  def getPlayerCount(self) -> int:
    with requests.Session() as rs:
      rs.mount('https://', requests.adapters.HTTPAdapter(
        max_retries=requests.urllib3.Retry(total=5, connect=5, read=5, backoff_factor=1)))
      with rs.get(f'{self.url}/api/v2/pokemon/', verify=True) as r:
        r.raise_for_status()
        json = r.json()
        return json["count"]

  def getPlayerById(self, id: int) -> Player:
    with requests.Session() as rs:
      rs.mount('https://', requests.adapters.HTTPAdapter(
        max_retries=requests.urllib3.Retry(total=5, connect=5, read=5, backoff_factor=1)))
      with rs.get(f'{self.url}/api/v2/pokemon/{id}', verify=True) as r:
        if r.status_code == 404:
          raise InvalidPlayerIdException
        r.raise_for_status()
        json = r.json()
        player = PokemonPlayer(id, json["name"], json["weight"], json["height"])
        return player

test_app.py:

import unittest
from unittest.mock import MagicMock, patch
from app import *

class TestPlayerRequestor(unittest.TestCase):
  def setUp(self):
    pass

  def tearDown(self):
    pass

  @patch('app.requests')
  def test_pokemon_player_requestor_raise_exception(self, mock_requests):
    mock_response = MagicMock()
    mock_response.status_code = 404
    mock_session = MagicMock()
    mock_requests.Session = mock_session
    instance = mock_session.return_value
    instance.get.return_value = mock_response
    requestor = PokemonPlayerRequestor('https://pokeapi.co')
    self.assertRaises(InvalidPlayerIdException, requestor.getPlayerById, 1)

Solution

  • You're on the right track, the context manager does interfere with your tests, since your code is calling the __enter__ dunder methods, only once, but twice!

    It helps printing your mocks to debug it if you get stuck in a mocking hell.

    This is what I got when I printed it:

    <MagicMock name='requests.Session().__enter__().get().__enter__()' id='4357377616'>

    I then rewrote your test as:

      @patch('app.requests')
      def test_pokemon_player_requestor_raise_exception(self, mock_requests):
        mock_session = MagicMock()
        mock_requests.Session = mock_session
    
        session_instance = MagicMock()
        mock_session().__enter__.return_value = session_instance
    
        mock_response = MagicMock()
        mock_response.status_code = 404
    
        session_instance.get().__enter__.return_value = mock_response
    
        requestor = PokemonPlayerRequestor('https://pokeapi.co')
        self.assertRaises(InvalidPlayerIdException, requestor.getPlayerById, 1)
    
    ➜  stackoverflow python -m unittest
    <MagicMock name='requests.Session().__enter__()' id='4357333536'>
    <MagicMock name='requests.Session().__enter__().get().__enter__()' id='4357377616'>
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.003s
    
    OK
    

    Hope that helps you.