Skip to main content

Mocking

Mocking is (temporary) replacing parts of your to-be-tested application with 'fake' objects within the scope of the test, so that you have full control over input and output within a certain test function. Python’s unittest library has built-in functionalities for mocking: https://docs.python.org/3/library/unittest.mock.html

Learn more?

The topic of 'mocking in Python' is extensively covered on the website: https://realpython.com/python-mock-library

When writing tests for your VIKTOR application, it is likely that you will need to mock certain VIKTOR functions. For example, API calls in your app encountered within a test will return a "OSError: Job token is not set" error, as the token is not available outside the app context.

The viktor.testing module provides various mock-objects to simplify the process of mocking certain VIKTOR objects.

VIKTOR components that require mocking:

Mocking API

API calls need to be mocked within the context of (automated) testing. There are two approaches you could take to achieve this:

  1. By using the mock_API decorator
  2. By isolating API actions in separate methods

Testing by using mock_API

New in v13.3.0
import unittest

from viktor.testing import mock_API

from app.my_entity_type.controller import MyEntityTypeController

class TestMyEntityTypeController(unittest.TestCase):
@mock_API()
def test_function(self):
MyEntityTypeController().function(...)

For each method that is called on the API class, the decorator will return mock objects:

  • MockedEntity representing Entity (e.g. when calling API.get_entity())
  • MockedEntityList representing EntityList (e.g. when calling API.get_root_entities())
  • MockedEntityRevision representing EntityRevision (e.g. when calling API.get_entity_revisions())
  • MockedEntityType representing EntityType (e.g. when calling Entity.entity_type)
  • MockedUser representing User (e.g. when calling API.get_current_user())

Each of these mock objects supports the properties and methods of their represented objects, for example, MockedEntity.last_saved_params mimics Entity.last_saved_params.

Assume the function() of the example above performs an API call to retrieve the child entities to count the number of red, green, and blue entities:

...

class MyEntityTypeController(ViktorController):
...

def function(self, entity_id):
red, green, blue = 0, 0, 0
children = API().get_entity_children(entity_id)
for child in children:
if child.last_saved_params.color == 'red':
red += 1
if child.last_saved_params.color == 'green':
green += 1
if child.last_saved_params.color == 'blue':
blue += 1
return red, green, blue

By default, the mock_API decorator returns a zero-length MockedEntityList when get_entity_children() is called. This means that, in the example above, function will return (0, 0, 0).

However, the decorator allows you to specify the outcome of each individual method such that we can actually test what would happen if a VIKTOR workspace consists of specific entities. For example, let's pass the entities that get_entity_children() in the example above should return, by providing a sequence of MockedEntity objects in the decorator:

import unittest

from viktor.testing import mock_API, MockedEntity

from app.my_entity_type.controller import MyEntityTypeController

CHILD_ENTITIES = [
MockedEntity(params={'color': 'red'}),
MockedEntity(params={'color': 'green'}),
MockedEntity(params={'color': 'green'}),
MockedEntity(params={'color': 'blue'}),
]

class TestMyEntityTypeController(unittest.TestCase):

@mock_API(get_entity_children=CHILD_ENTITIES)
def test_function(self):
red, green, blue = MyEntityTypeController().function(...)
self.assertEqual(red, 1)
self.assertEqual(green, 2)
self.assertEqual(blue, 1)

Instead of the default zero-length MockedEntityList, the decorator is now instructed to return the provided entities. Note that in this example the params are passed to MockedEntity, but you can define much more (e.g. parent / children / siblings etc.).

Testing by isolating API actions

Structuring the controller in such a way that each API action is isolated in a separate method makes it easier to maintain, debug, and reuse your code. Assume a controller that looks like this:

...

class MyEntityTypeController(ViktorController):
...

def get_child_params(self, entity_id):
# perform API actions
return child_params

def get_parent_params(self, entity_id):
# perform API actions
return parent_params

@GeometryView("3D model", duration_guess=3, x_axis_to_right=True)
def visualize(self, params, entity_id, **kwargs):
parent_params = self.get_parent_params(entity_id)
child_params = self.get_child_params(entity_id)
visualization = self.create_visualisation(parent_params, child_params)
return GeometryResult(visualization)

def download_file(self, params, entity_id, **kwargs):
parent_params = self.get_parent_params(entity_id)
download_content = self.generate_download_result(parent_params)
return DownloadResult(download_content)

If the visualize and download_file methods are now to be tested, we can make use of unittests mock.patch.object decorator in combination with mock.MagicMock to control the return value:

import unittest

from unittest import mock

from app.my_entity_type.controller import MyEntityTypeController

# params dictionaries
ENTITY_PARAMS = ...
PARENT_PARAMS = ...
CHILD_PARAMS = ...

class TestMyEntityTypeController(unittest.TestCase):

@mock.patch.object(MyEntityTypeController, 'get_child_params', mock.MagicMock(return_value=CHILD_PARAMS))
@mock.patch.object(MyEntityTypeController, 'get_parent_params', mock.MagicMock(return_value=PARENT_PARAMS))
def test_visualise(self):
MyEntityTypeController().visualize(params=ENTITY_PARAMS)

@mock.patch.object(MyEntityTypeController, 'get_child_params', mock.MagicMock(return_value=CHILD_PARAMS))
def test_generate_download_content(self):
MyEntityTypeController().generate_download_content(params=ENTITY_PARAMS)

Mocking deserialized params

The VIKTOR platform deserializes the raw params, such that they will enter the app code in their intuitive format. For example, an EntityOptionField will return an Entity object in the params instead of an entity_id. However, when you are using raw params in your tests (e.g. a JSON file), you will need to deserialize the params yourself. This is necessary when any of the following fields is used in the parametrization of the corresponding entity type:

  • DateField
  • EntityOptionField
  • ChildEntityOptionField
  • SiblingEntityOptionField
  • EntityMultiSelectField
  • ChildEntityMultiSelectField
  • SiblingEntityMultiSelectField
  • GeoPointField
  • GeoPolylineField
  • GeoPolygonField
  • FileField
  • MultiFileField

For example, when the parametrization consists of a NumberField, ChildEntityOptionField, and a FileField, the raw params could look like:

params = {
'number': 1,
'entity': 2, # corresponds to entity id
'file': 3, # corresponds to file resource id
}

Let's assume function() to perform an API call to

  • retrieve the last_saved_params of the selected child entity
  • retrieve the content of the selected file
...

class MyEntityTypeController(ViktorController):
...

def function(self, params):
child_params = params.entity.last_saved_params
file = params.file.file
...

The raw integers of params_dict should be converted to their corresponding mock objects (i.e. deserialized), in order for function() to succeed. There are two ways to achieve this:

  1. By defining a dictionary consisting of the mock objects manually

    from viktor import File
    from viktor.testing import MockedEntity, MockedFileResource

    params = {
    'number': 1,
    'entity': MockedEntity(name="My Entity"),
    'file': MockedFileResource(file=File.from_data("content"), filename="file.txt"),
    }
  2. By using the mock_params function, providing a JSON file consisting of the raw params along with the parametrization and (optionally) mocked resources

    from viktor import File
    from viktor.testing import MockedEntity, MockedFileResource, mock_params

    params = mock_params(
    params=File.from_path("path to JSON file"),
    parametrization=MyEntityTypeController.parametrization,
    entities={2: MockedEntity(name="My Entity")},
    file_resources={3: MockedFileResource(file=File.from_data("content"), filename="file.txt")},
    )

The function() can then be tested by providing the deserialized params:

...

class TestMyEntityTypeController(unittest.TestCase):

def test_function(self):
MyEntityTypeController().function(params=params)

Mocking ParamsFromFile

A file-type entity implements the @ParamsFromFile decorator on one of its controller methods. In order to test this method, the ParamsFromFile decorator (object) needs to be mocked (ParamsFromFile performs API calls internally). The viktor.testing module provides the mock_ParamsFromFile decorator to simplify this mocking:

import unittest

from viktor.testing import mock_ParamsFromFile

from app.my_entity_type.controller import MyEntityTypeController

class TestMyEntityTypeController(unittest.TestCase):

@mock_ParamsFromFile(MyEntityTypeController)
def test_process_file(self):
file = File.from_data("abc")
returned_dict = MyEntityTypeController().process_file(file)
self.assertDictEqual(returned_dict, {...})

Mocking View functions

New in v13.3.0

mock_View decorator for easier testing of view methods

View functions can be tested by mocking the decorator using mock_View:

import unittest

from viktor.testing import mock_View

from app.my_entity_type.controller import MyEntityTypeController

class TestMyEntityTypeController(unittest.TestCase):

@mock_View(MyEntityTypeController)
def test_my_view(self):
params = ...
result = MyEntityTypeController().my_view(params=params)
self.assertEqual(result, ...)

Please see each individual view guide for a specific example.

Mocking functions that call external worker

When calling the execute() method on a class that inherits from ExternalProgram, a job is sent to an external worker. These function calls need to be mocked within the context of (automated) testing.

Currently, the viktor.testing module provides the following decorators that facilitate mocking of workers:

Mocking functions that call external servers

There are various functions within the VIKTOR library that make use of (internal) services that are being processed on external servers. These external server calls cannot be performed outside the app-context and therefore these function calls need to be mocked within the context of (automated) testing.

The complete list of all functions that call external servers (hence require mocking):

  • viktor.external.dfoundations.BearingPilesModel.generate_input_file
  • viktor.external.dfoundations.TensionPilesModel.generate_input_file
  • viktor.external.dsettlement.Model1D.generate_input_file
  • viktor.external.dsettlement.Model2D.generate_input_file
  • viktor.external.idea_rcs.Model.generate_xml_input
  • viktor.external.idea_rcs.OpenModel.generate_xml_input
  • viktor.external.scia.Model.generate_xml_input
  • viktor.external.spreadsheet.SpreadsheetCalculation.evaluate
  • viktor.external.spreadsheet.SpreadsheetTemplate.render
  • viktor.external.spreadsheet.render_spreadsheet
  • viktor.external.word.WordFileTemplate.render
  • viktor.external.word.render_word_file
  • viktor.geo.GEFData.classify
  • viktor.geo.GEFFile.parse
  • viktor.utils.convert_excel_to_pdf
  • viktor.utils.convert_svg_to_pdf
  • viktor.utils.convert_word_to_pdf
  • viktor.utils.merge_pdf_files
  • viktor.utils.render_jinja_template