Unit and Integration Testing

Intro

Writing code to a code that already exists in order to verify correctness of function's work. That makes us sure the code works in accordance to programmer assumptions and software requirements.

Kind of tests:
  • Unit test - tests single unit of application f.e. we compare if a function returns expexted value or expected data type.
  • Integration test - tests interaction between modules being combined as a one group.
  • End2End test - tests from the end user’s experience by simulating the real user scenario.
When what test:
  • We use unit test when we want to test the logical side of the code without any calls to external services.
  • Integration tests are supposed to test functionality with a broader perspective, almost mimicking the behavior of a user. Because they connect to external systems and services, they take longer to run.
  • Simply put, We want to have a lot of unit tests that run quickly and all the time, and have integration tests running less often (for instance, on any new merge request)

Features

App includes following features:

  • Unit Tests
  • Integration Tests

Demo

App testing:
  • Application manages Blogs and its Posts.
  • Application is fed with data using JSON files.
  • Performing tests, I provide example data to module units i.e. functions and compare outcome with expected values.
  • I test application's single units:
    - testing class instantiation,
    - testing values returned by functions.
  • I test application's integration:
    - testing integration between classes,
    - testing main application methods.
  • All 12 test passed successfully.
Theory of Mocking:
  • There are applications that connect to external services as databases, storage services, external APIs, cloud services, and so on.
  • However, in our unit test we don't want to connect any database or issue HTTP request or send email notification every time we test our code. Thing we want to test is the pure logic behind our code with unit tests qickly, often and with no latency.
  • In this situation, we want to treat such code responsible for connecting with external service as a mock object. A mock object substitutes and imitates an object as close to the real object as possible. So in fact what we do is replacing an actuall call to the dependency with a mock object to make its bahaviour predictable. This way of mocking objects ensures control over the code’s behavior and espacially over unpredicted dependecies within a testing environment.
  • In addition, mock object provides information about its usage, which can be inspected, such as if I have called it or how many times I have called a particular dependency tha has been mocked.
  • With mocking an object we can prevent external dependencies from making a test fail. Let's take making HTTP requests to external service as the example. Test are being executed as expected only under the condition of a service's seccessful work. However, due to the fact the service is external and depends on an external infrastructure, it doesn't have to behave the same each time. Even a temporary change in the service's behaviour may cause a test's failure. In this case the solution is to replace such service with a mock object in order to simulate external service in a predictable way.
Mock's characteristics:
  • Mock object needs to simulate object that it replacs as close as possible. In order to get flexible enough, mock objct has so-called lazy attributes and methods that are created at the same time when we access them.
    from unittest.mock import Mock
    
    mock = Mock()
    
    print(mock)
    
    print(mock.some_attribute)
    print(mock.do_something())
    
    service = mock
    print(service.get_access().allow_admin())
    
    -----------
    # output
    # <Mock id='2594778696'>
    # <Mock name='mock.some_attribute' id='4394778696'>
    # <Mock name='mock.do_something()' id='4394778920'>
    # <Mock name='mock.get_access().allow_admin()' id='5394778920'>
    
    - Due to the fact mock creates arbitrary attributes and methods on the fly, it is suitable to replace any object along with its attrubutes and methods.
    - Notice that a returned object of each mocked object and its mocked method is also type of Mock (check outputs).
  • We can determine mocked functions's returned value up front. That way, we can fix functions, whose returned value may vary, to one value that will be returned every time.
    import datetime
    from unittest.mock import Mock
    
    # Save a couple of test days
    tuesday = datetime.datetime(year=2019, month=1, day=1)
    saturday = datetime.datetime(year=2019, month=1, day=5)
    
    # Mock datetime to control today's date
    datetime = Mock()
    
    def is_weekday():
      today = datetime.datetime.today()
      # Python's datetime library treats Monday as 0 and Sunday as 6
      return (0 <= today.weekday() < 5)
    
    # Mock .today() to return Tuesday
    datetime.datetime.today.return_value = tuesday
    # Test Tuesday is a weekday
    assert is_weekday()
    
    # Mock .today() to return Saturday
    datetime.datetime.today.return_value = saturday
    # Test Saturday is not a weekday
    assert not is_weekday()
    
    source: realpython.com
    - in the example above, we mock datetime object and assign return_value of tuesday then saturdayto mocked method datetime.today()
  • Let's mock HTTP request
    from unittest import mock
    import request
    
    def get_respond():
      response = requests.get("https://.../ip")
      if response.status_code == 200:
        return response.json()['origin']
    
    get_response()
    # '223.230.126.7'
    
    mock_response = mock.Mock()
    mock_response.json.return_value = {'origin': "0.0.0.0"}
    mock_response.status_code = 200
    
    mock_response.json()['origin']
    # '0.0.0.0'
    
    mock_response.status_code
    # 200
    
    - And now, when we compare object response with object mock_response there is no much differences between them, as they both have status_code attribute and they both have json() method which returns a dictionary with 'origin' key to retrieve a value.
    - The difference is that mock object json()['origin'] returns always the same value '0.0.0.0' no matter what.
    - We can also pass multiple key-word arguments when creating a mock object:
    mock_response = mock.Mock(**{"status_code": 200, "json.return_value": {'origin': "0.0.0.0"}})
    
    mock_response.json()['origin']
    # '0.0.0.0'
    
    mock_response.status_code
    # 200
    
    - From now on, we want to mock requests.get() that brings external dependecy.
    from unittest import mock
    import request
    
    def get_ip():
      response = requests.get("https://.../ip")
      if response.status_code == 200:
        return response.json()['origin']
    
    mock_response = mock.Mock(**{"status_code": 200, "json.return_value": {'origin': "0.0.0.0"}})
    mock_requests_get = mock.Mock(return_value=mock_response)
    
    mock_requests_get().status_code
    # 200
    
    mock_requests_get().json()
    # {'origin':'0.0.0.0'}
    
    requests.get = mock_requests_get
    get_ip()
    # '0.0.0.0'
    
    - In the code above, we can see that mock object mock_requests_get, as the function, returns another mock object which is mock_response.
    - By requests.get = mock_requests_get we mock the requests.get function that is used by get_ip().
    - A new mocked function gets the same attributes and methods:
    mock_requests_get().status_code
    # 200
    
    mock_requests_get().json()
    # {'origin':'0.0.0.0'}
    
    requests.get = mock_requests_get
    
    requests.get().status_code
    # 200
    
    requests.get().status_code
    # {'origin':'0.0.0.0'}
    
Coding some more Mocks:
  • Lets take a function that returns a value that is undpredictable on each call. Here, word 'unpredictable' is used in terms of getting different results every time the function runs:
    import random
    
    def roll_dice() -> int:
      return random.randint(1,6)
    
    print(roll_dice())
    print(roll_dice())
    print(roll_dice())
    
    -----------
    # output
    # 3
    # 6
    # 2
    
    - function returns number from range 1 - 6 inclusively.
    - this function has a certain level of uncertainity and should be mocked when we want to control it when testing.
  • Instantiating Mock type:
    from unittest import mock
    
    mock_roll_dice = mock.Mock(name="roll dice mock", return_value=1)
    print(mock_roll_dice())
    
    -----------
    # output
    # 1
    
    - we set a constant returned value of the function mock_roll_dice.
  • Mocking the function:
    roll_dice = mock_roll_dice
    print(roll_dice())
    
    -----------
    # output
    # 1
    
    - from now on, every time I call roll_dice function, it returns 1 every time (instead of randomn integer from the range) as it's the mock object right now.
Mocking objects in test environment:
  • Here is the Python game.py app that we want to test:
    def roll_dice():
      return random.randint(1,6)
    
    def guess_num(num: int) -> int:
    
      roll_num = roll_dice()
    
      if roll_num == num:
        return "You won!"
      else:
        return "You lost!"
    
    
  • test_game.py:
    from unittest import mock
    from game import guess_num
    
    @mock.patch("game.roll_dice")
    def test_guess_num(mock_roll_dice):
      mock_roll_dice.return_value = 3
      assert guess_num(3) == "You won!"
    
    
    - In the decorator, we indicate a function roll_dice (to be mocked) that is in the local scope of the function that we actually test - in this case guess_num function of game module.
    - Indicating function in the decorator passes its corresponding mock object to the unit test.
    - Function test_guess_num recieves it as mock_roll_dice then we can assign return_value of 3 to this mock object.
    - With this solution, we replace roll_dice function with another mocked object that throws 3 each time.
    - Then, calling guess_num with parameter of 3, we will always be winning getting "You won!" every time, getting test passed.
  • running test with pytest
    (myenv) C:\Users\U742905\Documents\mock>pytest
    ======================================= test session starts ==================================
    platform win32 -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
    rootdir: C:\Users\U742905\Documents\mock
    plugins: celery-4.4.7
    collected 1 item
    
    test_game.py .                                                 [100%]
    
    =======================================1 passed in 0.10s =====================================
    

Setup

Python libraries installation required:
pip install unittest2

Source Code

You can view the source code: HERE