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
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:
API
calls- Functions decorated with
@ParamsFromFile
- View functions (e.g. functions decorated with
@GeometryView
) - Functions that call external worker
- Functions that call external servers
Mocking API
API calls need to be mocked within the context of (automated) testing. There are two approaches you could take to achieve this:
- By using the
mock_API
decorator - By isolating API actions in separate methods
Testing by using mock_API
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
representingEntity
(e.g. when callingAPI.get_entity()
)MockedEntityList
representingEntityList
(e.g. when callingAPI.get_root_entities()
)MockedEntityRevision
representingEntityRevision
(e.g. when callingAPI.get_entity_revisions()
)MockedEntityType
representingEntityType
(e.g. when callingEntity.entity_type
)MockedUser
representingUser
(e.g. when callingAPI.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:
import viktor as vkt
class MyEntityTypeController(vkt.Controller):
...
def function(self, entity_id):
red, green, blue = 0, 0, 0
children = vkt.api_v1.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:
import viktor as vkt
class MyEntityTypeController(vkt.Controller):
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
@vkt.GeometryView("3D model", 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 vkt.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 vkt.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(vkt.Controller):
...
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:
-
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"),
} -
By using the
mock_params
function, providing a JSON file consisting of the raw params along with the parametrization and (optionally) mocked resourcesfrom 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
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:
mock_AxisVMAnalysis
(>= v13.5.0)mock_DFoundationsAnalysis
(>= v13.5.0)mock_DGeoStabilityAnalysis
(>= v13.5.0)mock_DSettlementAnalysis
(>= v13.5.0)mock_DSheetPilingAnalysis
(>= v13.5.0)mock_DStabilityAnalysis
(>= v13.5.0)mock_Excel
(>= v13.5.0)mock_GenericAnalysis
(>= v13.5.0)mock_GRLWeapAnalysis
(>= v13.5.0)mock_IdeaRcsAnalysis
(>= v13.5.0)mock_RFEMAnalysis
(>= v13.5.0)mock_RobotAnalysis
(>= v13.5.0)mock_SciaAnalysis
(>= v13.3.0)
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