Context Attributes and Functions¶
Other than Context Hooks, you can also configure contexts with any attributes or functions.
Attributes¶
You can set any arbitrary attribute from within any hook:
@context.before
def before(self):
self.calculator = Calculator()
and refer it later on:
@context.example
def is_a_calculaor(self):
assert type(self.calculator) == Calculator
Attributes and sub-contexts¶
While it is very intuitive to do self.attr = "value"
, when used with sub-contexts there’s potential for confusion:
from testslide.dsl import context
@context
def top_context(context):
@context.before
def set_attr(self):
self.attr = "top context value"
self.top_context_dict = {}
self.top_context_dict["attr"] = self.attr
@context.example
def attr_is_the_same(self):
self.assertEqual(self.attr, self.top_context_dict["attr"])
@context.sub_context
def sub_context(context):
@context.before
def reset_attr(self):
self.attr = "sub context value"
self.sub_context_dict = {}
self.sub_context_dict["attr"] = self.attr
@context.example
def attr_is_the_same(self):
self.assertEqual(self.attr, self.sub_context_dict["attr"]) # OK
self.assertEqual(self.attr, self.top_context_dict["attr"]) # Boom!
In this example self.attr
will have different values at top_context
and sub_context
resulting in some confusion in the assertions. These can be hard to spot in more complex scenarios, so TestSlide prevents attributes from being reset and the example above actually fails with AttributeError: Attribute 'attr' is already set.
.
The solution to this problem are memoized attributes.
Memoized Attributes¶
Memoized attributes are similar to a @property
but with 2 key differences:
Its value is materialized and cached on the first access.
When multiple contexts define the same memoized attribute the inner-most overrides the outer-most definitions.
Let’s see it in action:
from testslide.dsl import context
@context
def memoized_attributes(context):
@context.memoize
def memoized_list(self):
return []
@context.example
def can_access_memoized_attributes(self):
assert len(self.memoized_list) == 0 # list is materialized
self.memoized_list.append(True)
assert len(self.memoized_list) == 1 # same list is refereed
For the sake of convenience, memoized attributes can also be defined using lambdas:
context.memoize('memoized_list', lambda self: [])
or in bulk:
context.memoize(
memoized_list=lambda self: [],
yet_another_memoized_list=lambda self: [],
)
In some cases, delaying the materialization of the attribute is not desired and it can be forced to happen unconditionally from within a before hook:
@context.memoize_before
def memoized_list(self):
return []
Overriding Memoized Attributes¶
As memoized attributes from parent contexts can be overridden by defining a new value from an inner context, it not only gives consistency on the attribute value, but also allows for some powerful composition:
from testslide.dsl import context
from testslide import StrictMock
@context
def Composition(context):
context.memoize('attr_value', lambda self: 'default value')
@context.memoize
def mock(self):
mock = StrictMock()
mock.attr = self.attr_value
return mock
@context.example
def sees_default_value(self):
self.assertEqual(self.mock.attr, 'default value')
@context.sub_context
def With_different_value(context):
context.memoize('attr_value', lambda self: 'different value')
@context.example
def sees_different_value(self):
self.assertEqual(self.mock.attr, 'different value')
This means, sub-contexts can be used to “tweak” values from a parent context.
Functions¶
You can define arbitrary functions that can be called from test code with the @context.function
decorator:
@context
def Arbitrary_helper_functions(context):
@context.memoize
def some_list(self):
return []
# You can define arbitrary functions to call later
@context.function
def my_helper_function(self):
self.some_list.append('item')
return "I'm helping!"
@context.example
def can_call_helper_function(self):
assert "I'm helping!" == self.my_helper_function()
assert ['item'] == self.some_list