1
+ from unittest import TestCase
2
+ from unittest .mock import patch , MagicMock
3
+
4
+ import pytest
5
+
1
6
from business_rules import engine
2
7
from business_rules .variables import BaseVariables
3
8
from business_rules .operators import StringType
4
9
from business_rules .actions import BaseActions
5
10
6
- from mock import patch , MagicMock
7
- from unittest import TestCase
8
-
9
11
10
12
class EngineTests (TestCase ):
11
-
12
13
###
13
14
### Run
14
15
###
15
16
16
- @patch .object (engine , ' run' )
17
+ @patch .object (engine , " run" )
17
18
def test_run_all_some_rule_triggered (self , * args ):
18
- """ By default, does not stop on first triggered rule. Returns True if
19
+ """By default, does not stop on first triggered rule. Returns True if
19
20
any rule was triggered, otherwise False
20
21
"""
21
- rule1 = {' conditions' : ' condition1' , ' actions' : ' action name 1' }
22
- rule2 = {' conditions' : ' condition2' , ' actions' : ' action name 2' }
22
+ rule1 = {" conditions" : " condition1" , " actions" : " action name 1" }
23
+ rule2 = {" conditions" : " condition2" , " actions" : " action name 2" }
23
24
variables = BaseVariables ()
24
25
actions = BaseActions ()
25
26
26
27
def return_action1 (rule , * args , ** kwargs ):
27
- return rule ['actions' ] == 'action name 1'
28
+ return rule ["actions" ] == "action name 1"
29
+
28
30
engine .run .side_effect = return_action1
29
31
30
32
result = engine .run_all ([rule1 , rule2 ], variables , actions )
@@ -38,158 +40,198 @@ def return_action1(rule, *args, **kwargs):
38
40
self .assertTrue (result )
39
41
self .assertEqual (engine .run .call_count , 2 )
40
42
41
- @patch .object (engine , ' run' , return_value = True )
43
+ @patch .object (engine , " run" , return_value = True )
42
44
def test_run_all_stop_on_first (self , * args ):
43
- rule1 = {' conditions' : ' condition1' , ' actions' : ' action name 1' }
44
- rule2 = {' conditions' : ' condition2' , ' actions' : ' action name 2' }
45
+ rule1 = {" conditions" : " condition1" , " actions" : " action name 1" }
46
+ rule2 = {" conditions" : " condition2" , " actions" : " action name 2" }
45
47
variables = BaseVariables ()
46
48
actions = BaseActions ()
47
49
48
- result = engine .run_all ([rule1 , rule2 ], variables , actions ,
49
- stop_on_first_trigger = True )
50
+ result = engine .run_all (
51
+ [rule1 , rule2 ], variables , actions , stop_on_first_trigger = True
52
+ )
50
53
self .assertEqual (result , True )
51
54
self .assertEqual (engine .run .call_count , 1 )
52
55
engine .run .assert_called_once_with (rule1 , variables , actions )
53
56
54
- @patch .object (engine , ' check_conditions_recursively' , return_value = True )
55
- @patch .object (engine , ' do_actions' )
57
+ @patch .object (engine , " check_conditions_recursively" , return_value = True )
58
+ @patch .object (engine , " do_actions" )
56
59
def test_run_that_triggers_rule (self , * args ):
57
- rule = {' conditions' : ' blah' , ' actions' : ' blah2' }
60
+ rule = {" conditions" : " blah" , " actions" : " blah2" }
58
61
variables = BaseVariables ()
59
62
actions = BaseActions ()
60
63
61
64
result = engine .run (rule , variables , actions )
62
65
self .assertEqual (result , True )
63
66
engine .check_conditions_recursively .assert_called_once_with (
64
- rule ['conditions' ], variables )
65
- engine .do_actions .assert_called_once_with (rule ['actions' ], actions )
67
+ rule ["conditions" ], variables
68
+ )
69
+ engine .do_actions .assert_called_once_with (rule ["actions" ], actions )
66
70
67
-
68
- @patch .object (engine , 'check_conditions_recursively' , return_value = False )
69
- @patch .object (engine , 'do_actions' )
71
+ @patch .object (engine , "check_conditions_recursively" , return_value = False )
72
+ @patch .object (engine , "do_actions" )
70
73
def test_run_that_doesnt_trigger_rule (self , * args ):
71
- rule = {' conditions' : ' blah' , ' actions' : ' blah2' }
74
+ rule = {" conditions" : " blah" , " actions" : " blah2" }
72
75
variables = BaseVariables ()
73
76
actions = BaseActions ()
74
77
75
78
result = engine .run (rule , variables , actions )
76
79
self .assertEqual (result , False )
77
80
engine .check_conditions_recursively .assert_called_once_with (
78
- rule ['conditions' ], variables )
81
+ rule ["conditions" ], variables
82
+ )
79
83
self .assertEqual (engine .do_actions .call_count , 0 )
80
84
81
-
82
- @patch .object (engine , 'check_condition' , return_value = True )
83
- def test_check_all_conditions_with_all_true (self , * args ):
84
- conditions = {'all' : [{'thing1' : '' }, {'thing2' : '' }]}
85
- variables = BaseVariables ()
86
-
87
- result = engine .check_conditions_recursively (conditions , variables )
88
- self .assertEqual (result , True )
89
- # assert call count and most recent call are as expected
90
- self .assertEqual (engine .check_condition .call_count , 2 )
91
- engine .check_condition .assert_called_with ({'thing2' : '' }, variables )
92
-
93
-
94
- ###
95
- ### Check conditions
96
- ###
97
- @patch .object (engine , 'check_condition' , return_value = False )
98
- def test_check_all_conditions_with_all_false (self , * args ):
99
- conditions = {'all' : [{'thing1' : '' }, {'thing2' : '' }]}
100
- variables = BaseVariables ()
101
-
102
- result = engine .check_conditions_recursively (conditions , variables )
103
- self .assertEqual (result , False )
104
- engine .check_condition .assert_called_once_with ({'thing1' : '' }, variables )
105
-
106
-
107
- def test_check_all_condition_with_no_items_fails (self ):
108
- with self .assertRaises (AssertionError ):
109
- engine .check_conditions_recursively ({'all' : []}, BaseVariables ())
110
-
111
-
112
- @patch .object (engine , 'check_condition' , return_value = True )
113
- def test_check_any_conditions_with_all_true (self , * args ):
114
- conditions = {'any' : [{'thing1' : '' }, {'thing2' : '' }]}
115
- variables = BaseVariables ()
116
-
117
- result = engine .check_conditions_recursively (conditions , variables )
118
- self .assertEqual (result , True )
119
- engine .check_condition .assert_called_once_with ({'thing1' : '' }, variables )
120
-
121
-
122
- @patch .object (engine , 'check_condition' , return_value = False )
123
- def test_check_any_conditions_with_all_false (self , * args ):
124
- conditions = {'any' : [{'thing1' : '' }, {'thing2' : '' }]}
125
- variables = BaseVariables ()
126
-
127
- result = engine .check_conditions_recursively (conditions , variables )
128
- self .assertEqual (result , False )
129
- # assert call count and most recent call are as expected
130
- self .assertEqual (engine .check_condition .call_count , 2 )
131
- engine .check_condition .assert_called_with ({'thing2' : '' }, variables )
132
-
133
-
134
- def test_check_any_condition_with_no_items_fails (self ):
135
- with self .assertRaises (AssertionError ):
136
- engine .check_conditions_recursively ({'any' : []}, BaseVariables ())
137
-
138
-
139
- def test_check_all_and_any_together (self ):
140
- conditions = {'any' : [], 'all' : []}
141
- variables = BaseVariables ()
142
- with self .assertRaises (AssertionError ):
143
- engine .check_conditions_recursively (conditions , variables )
144
-
145
- @patch .object (engine , 'check_condition' )
146
- def test_nested_all_and_any (self , * args ):
147
- conditions = {'all' : [
148
- {'any' : [{'name' : 1 }, {'name' : 2 }]},
149
- {'name' : 3 }]}
150
- bv = BaseVariables ()
151
-
152
- def side_effect (condition , _ ):
153
- return condition ['name' ] in [2 ,3 ]
154
- engine .check_condition .side_effect = side_effect
155
-
156
- engine .check_conditions_recursively (conditions , bv )
157
- self .assertEqual (engine .check_condition .call_count , 3 )
158
- engine .check_condition .assert_any_call ({'name' : 1 }, bv )
159
- engine .check_condition .assert_any_call ({'name' : 2 }, bv )
160
- engine .check_condition .assert_any_call ({'name' : 3 }, bv )
161
-
162
-
163
85
###
164
86
### Operator comparisons
165
87
###
166
88
def test_check_operator_comparison (self ):
167
- string_type = StringType (' yo yo' )
168
- with patch .object (string_type , ' contains' , return_value = True ):
89
+ string_type = StringType (" yo yo" )
90
+ with patch .object (string_type , " contains" , return_value = True ):
169
91
result = engine ._do_operator_comparison (
170
- string_type , 'contains' , 'its mocked' )
92
+ string_type , "contains" , "its mocked"
93
+ )
171
94
self .assertTrue (result )
172
- string_type .contains .assert_called_once_with ('its mocked' )
173
-
95
+ string_type .contains .assert_called_once_with ("its mocked" )
174
96
175
97
###
176
98
### Actions
177
99
###
178
100
def test_do_actions (self ):
179
- actions = [ {'name' : 'action1' },
180
- {'name' : 'action2' ,
181
- 'params' : {'param1' : 'foo' , 'param2' : 10 }}]
101
+ actions = [
102
+ {"name" : "action1" },
103
+ {"name" : "action2" , "params" : {"param1" : "foo" , "param2" : 10 }},
104
+ ]
182
105
defined_actions = BaseActions ()
183
106
defined_actions .action1 = MagicMock ()
184
107
defined_actions .action2 = MagicMock ()
185
108
186
109
engine .do_actions (actions , defined_actions )
187
110
188
111
defined_actions .action1 .assert_called_once_with ()
189
- defined_actions .action2 .assert_called_once_with (param1 = ' foo' , param2 = 10 )
112
+ defined_actions .action2 .assert_called_once_with (param1 = " foo" , param2 = 10 )
190
113
191
114
def test_do_with_invalid_action (self ):
192
- actions = [{' name' : ' fakeone' }]
115
+ actions = [{" name" : " fakeone" }]
193
116
err_string = "Action fakeone is not defined in class BaseActions"
194
117
with self .assertRaisesRegex (AssertionError , err_string ):
195
118
engine .do_actions (actions , BaseActions ())
119
+
120
+
121
+ @patch .object (engine , "check_condition" , return_value = True )
122
+ def test_check_all_conditions_with_all_true (_mock_check_condition : MagicMock ) -> None :
123
+ # set up
124
+ conditions = {"all" : [{"thing1" : "" }, {"thing2" : "" }]}
125
+ variables = BaseVariables ()
126
+ # test
127
+ result = engine .check_conditions_recursively (conditions , variables )
128
+ assert result is True
129
+ # assert call count and most recent call are as expected
130
+ assert engine .check_condition .call_count == 2
131
+ engine .check_condition .assert_called_with ({"thing2" : "" }, variables )
132
+
133
+
134
+ @patch .object (engine , "check_condition" , return_value = False )
135
+ def test_check_all_conditions_with_all_false (_mock_check_condition : MagicMock ) -> None :
136
+ # set up
137
+ conditions = {"all" : [{"thing1" : "" }, {"thing2" : "" }]}
138
+ variables = BaseVariables ()
139
+ # test
140
+ result = engine .check_conditions_recursively (conditions , variables )
141
+ # self.assertEqual(result, False)
142
+ assert result is False
143
+ engine .check_condition .assert_called_once_with ({"thing1" : "" }, variables )
144
+
145
+
146
+ def test_check_all_and_any_together () -> None :
147
+ conditions = {"any" : [], "all" : []}
148
+ variables = BaseVariables ()
149
+ with pytest .raises (
150
+ ValueError ,
151
+ match = "Only one of 'not', 'any', or 'all' can be at the same level in the conditions dict" ,
152
+ ):
153
+ engine .check_conditions_recursively (conditions , variables )
154
+
155
+
156
+ def test_check_all_conditions_with_no_items_fails () -> None :
157
+ with pytest .raises (AssertionError ):
158
+ engine .check_conditions_recursively ({"all" : []}, BaseVariables ())
159
+
160
+
161
+ @patch .object (engine , "check_condition" , return_value = True )
162
+ def test_check_any_conditions_with_all_true (_mock_check_condition : MagicMock ) -> None :
163
+ conditions = {"any" : [{"thing1" : "" }, {"thing2" : "" }]}
164
+ variables = BaseVariables ()
165
+
166
+ result = engine .check_conditions_recursively (conditions , variables )
167
+ assert result is True
168
+ engine .check_condition .assert_called_once_with ({"thing1" : "" }, variables )
169
+
170
+
171
+ @patch .object (engine , "check_condition" , return_value = False )
172
+ def test_check_any_conditions_with_all_false (_mock_check_condition : MagicMock ) -> None :
173
+ conditions = {"any" : [{"thing1" : "" }, {"thing2" : "" }]}
174
+ variables = BaseVariables ()
175
+
176
+ result = engine .check_conditions_recursively (conditions , variables )
177
+ assert result is False
178
+ # assert call count and most recent call are as expected
179
+ assert engine .check_condition .call_count == 2
180
+ engine .check_condition .assert_called_with ({"thing2" : "" }, variables )
181
+
182
+
183
+ def test_check_any_condition_with_no_items_fails () -> None :
184
+ with pytest .raises (AssertionError ):
185
+ engine .check_conditions_recursively ({"any" : []}, BaseVariables ())
186
+
187
+
188
+ @patch .object (engine , "check_condition" )
189
+ def test_nested_all_and_any (_mock_check_condition : MagicMock ) -> None :
190
+ conditions = {"all" : [{"any" : [{"name" : 1 }, {"name" : 2 }]}, {"name" : 3 }]}
191
+ bv = BaseVariables ()
192
+
193
+ def side_effect (condition , _ ):
194
+ return condition ["name" ] in [2 , 3 ]
195
+
196
+ engine .check_condition .side_effect = side_effect
197
+
198
+ engine .check_conditions_recursively (conditions , bv )
199
+ # self.assertEqual(engine.check_condition.call_count, 3)
200
+ assert engine .check_condition .call_count == 3
201
+ engine .check_condition .assert_any_call ({"name" : 1 }, bv )
202
+ engine .check_condition .assert_any_call ({"name" : 2 }, bv )
203
+ engine .check_condition .assert_any_call ({"name" : 3 }, bv )
204
+
205
+
206
+ @pytest .mark .parametrize ("other_key" , ["all" , "any" ])
207
+ def test_check_not_with_all_any (other_key : str ) -> None :
208
+ """
209
+ DOCUMENT ME.
210
+ """
211
+ conditions = {"not" : [], other_key : []}
212
+ variables = BaseVariables ()
213
+ with pytest .raises (
214
+ ValueError ,
215
+ match = "Only one of 'not', 'any', or 'all' can be at the same level in the conditions dict" ,
216
+ ):
217
+ engine .check_conditions_recursively (conditions , variables )
218
+
219
+
220
+ @pytest .mark .parametrize (
221
+ "check_condition_result, expected_result" , [(True , False ), (False , True )]
222
+ )
223
+ def test_check_not_negates_result (
224
+ check_condition_result : bool , expected_result : bool
225
+ ) -> None :
226
+ """
227
+ DOCUMENT ME.
228
+ """
229
+ conditions = {"not" : {"thing1" : "" }}
230
+ variables = BaseVariables ()
231
+
232
+ with patch .object (
233
+ engine , "check_condition" , return_value = check_condition_result
234
+ ) as mock_check_condition :
235
+ result = engine .check_conditions_recursively (conditions , variables )
236
+ assert result is expected_result
237
+ mock_check_condition .assert_called_once_with ({"thing1" : "" }, variables )
0 commit comments