Demonstrations of select features of the TestScribe tool.
Table of Contents
- Mock a class instance
- Create a class instance
- A real-world example of an end-to-end test scenario and how to update it
- Exception result
- Test a method
- Test multiple input sets
- Multiple class instances in a list
- Raise an exception in a mock call
- Ignore the return value of a mock call
- Patch a function
- Patch a string
- Input alias
- Wrapper function
- Annotate a class instance member variable with type information
- Results that contain class instances
- Scribe files
- A real world example of how the tool makes it easy to make some batch updates to tests
- Assert class instances in mock call parameters
Here is the demo project.
It’s intentionally made as simple as possible to make it easier to understand.
Feel free to download it and play with it.
Recommend setting up the IDE quick launch to make it easier to try the examples.
For more realistic examples, see how the tool uses itself to test here.
Mock a class instance
The function search_name takes a Service object as a parameter. The service may involve a database or a network call. It’s easier to test by mocking it.
An example test run output:
...
Please provide the value for the parameter (service) of type: (tsdemo.service.Service) []: m
Created a mock: Mock: name (m_service) spec (<class 'tsdemo.service.Service'>)
Please provide the value for the parameter (keyword) of type: (str) []: Bob
Calling search_name(service=Mock: name (m_service) spec (<class 'tsdemo.service.Service'>), keyword='Bob')
m_service's search_a_name method is called
with: keyword='key: Bob'.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/simple_mock.py", line 11, in search_name
name = service.search_a_name("key: " + keyword)
Please provide the value for the return value of type: (str) []: real Bob
Mock call return value: 'real Bob'
***** Result:
type: <class 'str'>
value:
{"name": "real Bob"}
***** Result end
...
Here is the generated unit test code.
Here is a demo video
Create a class instance
The function get_person_age takes a Person object as a parameter.
An example test run output:
...
Please provide the value for the parameter (p) of type: (tsdemo.person.Person) []: c("Bob", 10)
Calling get_person_age(p=tsdemo.person.Person("Bob", 10))
***** Result:
type: <class 'int'>
value:
10
***** Result end...
Here is the generated unit test code.
A real-world example of an end-to-end test scenario and how to update it
The tool uses itself to test. See this FAQ for more details.
test_retry_invalid_input_output is a generated test targeting a wrapper function retry_invalid_input_output. The wrapper function encapsulates the testing logic of running an end-to-end testing session with an invalid input and return the command line output.
The following demo video demonstrates how this test can be easily updated when the error message is changed.
Here is the commit diff of the change.
Updating the test only involves a few routine key presses and visual inspection.
Here is the updated test.
Exception result
When a test run results in an exception, the tool can display the exception information and generate the appropriate test code.
This target function always_raise_exception will always raise an exception.
An example test run output:
...
Calling always_raise_exception()
The function always_raise_exception throws an exception.
Traceback (most recent call last):
File "/home/ray/code/testscribe/src/testscribe/execution.py", line 86, in get_args_and_call
ret_value = call_target_function(func, args)
File "/home/ray/code/testscribe/src/testscribe/execution_util.py", line 87, in call_target_function
return func(*binded_args.args, **binded_args.kwargs)
File "/home/ray/code/testscribe-demo/tsdemo/raise_exception.py", line 7, in always_raise_exception
raise Exception("test exception")
Exception: test exception
...
Here is the generated test.
Test a method
When the target function is a method of a class, the tool will automatically call the constructor to create an instance first and invoke the method on that instance.
This method greet is the target.
An example test run output:
...
Prepare to create an instance of the class: Greeter
Getting parameters for the function (Greeter)
Please provide the value for the parameter (my_name) of type: (str) []: Bob
Calling Greeter(my_name='Bob')
Prepare to call the target function.
Getting parameters for the function (Greeter.greet)
Please provide the value for the parameter (to) of type: (str) []: Alice
Calling Greeter.greet(to='Alice')
***** Result:
type: <class 'str'>
value:
Hello Alice. My name is Bob
***** Result end
...
Here is the generated unit test code.
Test multiple input sets
See this FAQ for the background information.
The eval_expression function takes an expression as a string and returns its evaluation result. We will test different expressions with multiple test runs.
Here is a demo video comparing testing with TestScribe with the traditional testing approach.
TestScribe makes you about 5 times more productive in this test scenario ( 30 seconds vs 140 seconds ).
This assumes perfect execution of the traditional test method.
In practice, looking up documentation and fixing errors usually takes additional time.
More importantly, the saved steps are the boring parts.
Here are the test runs:
...
Please provide the value for the parameter (expr) of type: (str) []: 3 + 5
Calling eval_expression(expr='3 + 5')
***** Result:
type: <class 'int'>
value:
8
***** Result end
Test name: [_]: _addition
...
...
Please provide the value for the parameter (expr) of type: (str) [3 + 5]: 2 + 4
Calling eval_expression(expr='2 + 4')
***** Result:
type: <class 'int'>
value:
6
***** Result end
Test name: [_]: _addition
...
...
Please provide the value for the parameter (expr) of type: (str) [2 + 4]: 6 * 9
Calling eval_expression(expr='6 * 9')
***** Result:
type: <class 'int'>
value:
54
***** Result end
Test name help: 'test_' prefix will be added automatically. Use a leading '_' to include the target function name as part of the prefix.
Test name: [_]: _multiplication
...
You only need to type in the expressions (3+5, 2+4, 6 * 9) and the test names (_addition, _multiplication).
Here is the generated tests.
Here is the manually written test for comparison.
Try for yourself to see which method (manual vs using the tool) makes you and people who read your tests more productive for this test.
Multiple class instances in a list
The function get_average_age takes a list of Person object as a parameter.
Create multiple real instances in a list
An example test run:
...
Please provide the value for the parameter (person_list) of type: (typing.List[tsdemo.person.Person]) []: [tsdemo.person.Person("a", 2), tsdemo.person.Person("b", 3)]
Calling get_average_age(person_list=[tsdemo.person.Person("a", 2), tsdemo.person.Person("b", 3)])
***** Result:
type: <class 'int'>
value:
2
***** Result end
...
Create multiple mocks in a list
An example test run:
Please provide the value for the parameter (person_list) of type: (typing.List[tsdemo.person.Person]) []: [m, m]
Created a mock: Mock: name (m_person) spec (<class 'tsdemo.person.Person'>)
Created a mock: Mock: name (m_person_1) spec (<class 'tsdemo.person.Person'>)
Calling get_average_age(person_list=[<testscribe.mock_proxy.MockProxy object at 0x7f43dd744af0>, <testscribe.mock_proxy.MockProxy object at 0x7f43dd744e50>])
Mock object m_person's ( age ) attribute is accessed for the first time.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/objects_in_list.py", line 12, in get_average_age
total += p.age
Please provide the value for the age attribute of type: (int) []: 2
Mock attribute value: 2
Mock object m_person_1's ( age ) attribute is accessed for the first time.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/objects_in_list.py", line 12, in get_average_age
total += p.age
Please provide the value for the age attribute of type: (int) []: 3
Mock attribute value: 3
***** Result:
type: <class 'int'>
value:
2
***** Result end
Here is the generated unit test code.
Raise an exception in a mock call
The function search_name takes a Service object as a parameter. The search_a_name method on that Service object is called. An example test run output to test the behavior when the call throws an exception:
...
Please provide the value for the parameter (service) of type: (tsdemo.service.Service) [m(tsdemo.service.Service, 'm_service')]:
Created a mock: Mock: name (m_service) spec (<class 'tsdemo.service.Service'>)
Please provide the value for the parameter (keyword) of type: (str) [Bob]:
Calling search_name(service=Mock: name (m_service) spec (<class 'tsdemo.service.Service'>), keyword='Bob')
m_service's search_a_name method is called
with: keyword='key: Bob'.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/simple_mock.py", line 11, in search_name
name = service.search_a_name("key: " + keyword)
Please provide the value for the return value of type: (str) [real Bob]: throw(Exception("search failed"))
Mock call return value: InputValue(expression='throw(Exception("search failed"))', value=Exception('search failed'))
The function search_name throws an exception.
...
Exception: search failed
Test name help: 'test_' prefix will be added automatically. Use a leading '_' to include the target function name as part of the prefix.
Test name: [_]: _exception is propagated
...
Notice that this test run use an earlier test run’s input as default input.
Here is the generated unit test code.
Ignore the return value of a mock call
The function show takes a Printer object as a parameter. The display method on that Printer object is called. The returned string is not used by this function. It’s easier to ignore this return value when the object is mocked during a test. And the generated test is simpler.
An example test run output to ignore the mocked display call:
...
Please provide the value for the parameter (text) of type: (str) []: a
Please provide the value for the parameter (printer) of type: (tsdemo.ignore_mock_return.Printer) []: m
Created a mock: Mock: name (m_printer) spec (<class 'tsdemo.ignore_mock_return.Printer'>)
Calling show(text='a', printer=Mock: name (m_printer) spec (<class 'tsdemo.ignore_mock_return.Printer'>))
m_printer's display method is called
with: text='a'.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/ignore_mock_return.py", line 17, in show
printer.display(text)
Please provide the value for the return value of type: (str) []: ignore
Mock call return value: InputValue(expression='ignore', value='Ignored')
...
Here is the generated unit test code.
Patch a function
The function call_fixed_func has a fixed dependency on the calculate function.
To replace the calculate function with a mock object for a test, add the following code to the setup function
patch_with_mock(calculate)
The setup function definition is here. Uncomment the line before starting a test run which specifies this setup function. The configuration file is here
The setup function is called before the target function is called. The patch_with_mock function replaces the target function with a mock object of the same type as the target function.
...
Calling the setup function setup.setup.
setup function in tests is called.
Created a mock: Mock: name (m_calculate) spec (<function calculate at 0x7fe62436eb80>)
Patch tsdemo.patch_function.calculate with Mock: name (m_calculate) spec (<function calculate at 0x7fe62436eb80>)
Prepare to call the target function.
Getting parameters for the function (call_fixed_func)
Please provide the value for the parameter (num) of type: (int) []: 2
Calling call_fixed_func(num=2)
m_calculate is called
with: seed=2.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/patch_function.py", line 12, in call_fixed_func
return calculate(num)
Please provide the value for the return value of type: (int) []: 1
Mock call return value: 1
***** Result:
type: <class 'int'>
value:
1
***** Result end
...
Here is the generated unit test code.
Patch a string
The function get_db_name has a fixed dependency on the DB_NAME variable.
To replace the string for a test, add the following code to the setup function
patch_with_expression(target_str="tsdemo.patch_string.DB_NAME", expression="'test'")
Notice that the string value has to be quoted to be a valid Python expression.
The setup function definition is here. Uncomment the line before starting a test run which specifies this setup function.
Test run output:
...
setup function in tests is called.
Patch tsdemo.patch_string.DB_NAME with 'test'
Prepare to call the target function.
Getting parameters for the function (get_db_name)
Calling get_db_name()
***** Result:
type: <class 'str'>
value:
test
***** Result end
...
Here is the generated unit test code.
Input alias
To make inputs easier, you can define aliases.
In the example create real instances in a list, instead of typing the fully qualified class name, you can create an alias for the class name by adding the following to the setup function.
define_alias(alias=”ps”, full_str=”tsdemo.person.Person”)
Here is a test run which produces the same result as the example referenced above.
Please provide the value for the parameter (person_list) of type: (typing.List[tsdemo.person.Person]) []: [ps("a", 2), ps("b", 3)]
Expanded alias: ps 2 times.
Result after the expansion: [tsdemo.person.Person("a", 2), tsdemo.person.Person("b", 3)]
Calling get_average_age(person_list=[tsdemo.person.Person("a", 2), tsdemo.person.Person("b", 3)])
Wrapper function
To test a magic string method here, a wrapper function is used.
Here is the generated test code.
Annotate a class instance member variable with type information
Proper type information makes input easier in some cases.
In this class the model field is annotated with type information. For comparison, the owner field is not.
As the result, the model field input doesn’t need to be quoted. The owner field input does.
Here is an example test run:
...
Please provide the value for the parameter (car) of type: (tsdemo.annotate_field_type.Car) []: m
Created a mock: Mock: name (m_car) spec (<class 'tsdemo.annotate_field_type.Car'>)
Calling get_car_info(car=Mock: name (m_car) spec (<class 'tsdemo.annotate_field_type.Car'>))
Mock object m_car's ( model ) attribute is accessed for the first time.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/annotate_field_type.py", line 15, in get_car_info
return f"Car model: {car.model}, owner: {car.owner}"
Please provide the value for the model attribute of type: (str) []: camery
Mock attribute value: 'camery'
Mock object m_car's ( owner ) attribute is accessed for the first time.
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/annotate_field_type.py", line 15, in get_car_info
return f"Car model: {car.model}, owner: {car.owner}"
Please provide the value for the owner attribute of type: (any) []: "Bob"
Mock attribute value: 'Bob'
***** Result:
type: <class 'str'>
value:
Car model: camery, owner: Bob
***** Result end
...
Results that contain class instances
The class doesn’t implement a custom __repr__ method
The result display and generated assertions will be based on member fields.
The target function create_simple_class_instance returns an instance of the class SimpleClass, which doesn’t define a custom __repr__ method.
Here is an example test run:
...
***** Result:
type: tsdemo.object_result.SimpleClass
value:
Object(type (tsdemo.object_result.SimpleClass), members ({'str_field': 'a', 'int_field': 1}))
***** Result end
...
Here is the generated test code.
The class implements a custom __repr__ method
The result display and generated assertions will be based on the result of repr(object).
The target function create_class_with_repr_instance returns an instance of the class ClassWithRepr, which defines a custom __repr__ method.
Here is an example test run:
...
***** Result:
type: tsdemo.object_with_repr_result.ClassWithRepr
value:
Object(type (tsdemo.object_with_repr_result.ClassWithRepr), repr (ClassWithRepr(str_field='a', int_field=1)))
***** Result end
...
Here is the generated test code.
Scribe files
The tool generates machine-readable YAML formatted files with the tscribe extension alongside the unit test files.
Here is a simple example that tests an add function.
You can find more examples generated for other demos on this page here.
A real world example of how the tool makes it easy to make some batch updates to tests
This change fixed the order of a generated assertion after an exception is thrown. The expected value should be on the right hand side of the equation.
After making this change to the production code, running the sync-all command once updated all the generated tests.
Assert class instances in mock call parameters
The tool automatically generates assertion for complex mock call parameters such as class instances. This would be cumbersome to do manually.
The target function call_b_method calls a method on its parameter with an instance of the class A.
Here is an example test run:
...
Please provide the value for the parameter (b) of type: (tsdemo.object_in_mock_call_param.B) []: m
Created a mock: Mock: name (m_b) spec (<class 'tsdemo.object_in_mock_call_param.B'>)
Calling call_b_method(b=Mock: name (m_b) spec (<class 'tsdemo.object_in_mock_call_param.B'>))
m_b's do method is called
with: a=tsdemo.object_in_mock_call_param.A(i=1).
Call stack:
File "/home/ray/code/testscribe-demo/tsdemo/object_in_mock_call_param.py", line 15, in call_b_method
b.do(A(1))
...
Here is the generated test code.