Skip to content

Commit 7501f31

Browse files
committed
Completed implementation making run_ not required.
1 parent 706f0a2 commit 7501f31

8 files changed

Lines changed: 164 additions & 47 deletions

File tree

README.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ test_name (19.43149897100011 seconds)
108108

109109
### Test Cases
110110

111-
A test fixture can contain 1 or mote test cases. Test cases are discovered when execute_tests() is called on the test fixture. Every test case is comprised of 2 required and 2 optional methods and are discovered by the following convention: prefix_testname, where valid prefixes are: before_, run_, assertion_, and after_. A test fixture that has run_fred and assertion_fred methods has 1 test case called 'fred'. The following are details about test case methods:
111+
A test fixture can contain 1 or mote test cases. Test cases are discovered when execute_tests() is called on the test fixture. Every test case is comprised of 1 required and 3 optional methods and are discovered by the following convention: prefix_testname, where valid prefixes are: before_, run_, assertion_, and after_. A test fixture that has run_fred and assertion_fred methods has 1 test case called 'fred'. The following are details about test case methods:
112112

113113
* _before\_(testname)_ - (optional) - if provided, is run prior to the 'run_' method. This method can be used to setup any test pre-conditions
114114

115-
* _run\_(testname)_ - (required) - run after 'before_' if before was provided, otherwise run first. This method typically runs the notebook under test
115+
* _run\_(testname)_ - (optional) - if provider, is run after 'before_' if before was provided, otherwise run first. This method is typically used to run the notebook under test
116116

117-
* _assertion\_(testname)_ (required) - run after 'run_'. This method typically contains the test assertions
117+
* _assertion\_(testname)_ (required) - run after 'run_', if run was provided. This method typically contains the test assertions
118118

119119
__Note:__ You can assert test scenarios using the standard ``` assert ``` statement or the assertion capabilities from a package of your choice.
120120

@@ -144,7 +144,7 @@ print(result.to_string())
144144

145145
### before_all and after_all
146146

147-
Test Fixtures also can have a before_all() method which is run prior to all tests and an after_all() which is run after all tests.
147+
Test Fixtures also can have a before_all() method which is run prior to all tests and an after_all() which is run after all tests.
148148

149149
``` Python
150150
from runtime.nutterfixture import NutterFixture, tag
@@ -162,6 +162,42 @@ class MultiTestFixture(NutterFixture):
162162
163163
```
164164

165+
### Multiple test assertions pattern with before_all
166+
167+
It is possible to support multiple assertions for a test by implementing a before_all method, no run methods and multiple assertion methods. In this pattern, the before_all method runs the notebook under test. There are no run methods. The assertion methods simply assert against what was done in before_all.
168+
169+
``` Python
170+
from runtime.nutterfixture import NutterFixture, tag
171+
class MultiTestFixture(NutterFixture):
172+
def before_all(self):
173+
dbutils.notebook.run('notebook_under_test', 600, args)
174+
175+
176+
def assertion_test_case_1(self):
177+
178+
179+
def assertion_test_case_2(self):
180+
181+
182+
def after_all(self):
183+
184+
```
185+
186+
### Guaranteed test order
187+
188+
After test cases are loaded, Nutter uses a sorted dictionary to order them by name. Therefore test cases will be executed in alphabetical order.
189+
190+
### Sharing state between test cases
191+
192+
It is possible to share state across test cases via instance variables. Generally, these should be set in the constructor. Please see below:
193+
194+
```Python
195+
class TestFixture(NutterFixture):
196+
def __init__(self):
197+
self.file = '/data/myfile'
198+
NutterFixture.__init__(self)
199+
```
200+
165201
## Nutter CLI
166202

167203
The Nutter CLI is a command line interface that allows you to execute and list tests via a Command Prompt.

cli/nuttercli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from .reportsman import ReportWriters
1818
from . import reportsman as reports
1919

20-
__version__ = '0.1.32'
20+
__version__ = '0.1.33'
2121

2222
BUILD_NUMBER_ENV_VAR = 'NUTTER_BUILD_NUMBER'
2323

runtime/nutterfixture.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from common.testresult import TestResults
99
from .fixtureloader import FixtureLoader
1010
from common.testexecresults import TestExecResults
11+
from collections import OrderedDict
1112

1213

1314
def tag(the_tag):
@@ -57,7 +58,7 @@ def __load_fixture(self):
5758
if test_case_dict is None:
5859
logging.fatal("Invalid Test Fixture")
5960
raise InvalidTestFixtureException("Invalid Test Fixture")
60-
self.__test_case_dict = test_case_dict
61+
self.__test_case_dict = OrderedDict(sorted(test_case_dict.items(), key=lambda t: t[0]))
6162

6263
logging.debug("Found {} test cases".format(len(test_case_dict)))
6364
for key, value in self.__test_case_dict.items():

runtime/testcase.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ def get_testcase(test_name):
1717

1818

1919
class TestCase():
20-
ERROR_MESSAGE_RUN_MISSING = """ TestCase does not contain a run function.
21-
Please pass a function to set_run"""
2220
ERROR_MESSAGE_ASSERTION_MISSING = """ TestCase does not contain an assertion function.
2321
Please pass a function to set_assertion """
2422

@@ -27,6 +25,7 @@ def __init__(self, test_name):
2725
self.before = None
2826
self.__before_set = False
2927
self.run = None
28+
self.__run_set = False
3029
self.assertion = None
3130
self.after = None
3231
self.__after_set = False
@@ -39,6 +38,7 @@ def set_before(self, before):
3938

4039
def set_run(self, run):
4140
self.run = run
41+
self.__run_set = True
4242

4343
def set_assertion(self, assertion):
4444
self.assertion = assertion
@@ -60,7 +60,8 @@ def execute_test(self):
6060
"Both a run and an assertion are required for every test")
6161
if self.__before_set and self.before is not None:
6262
self.before()
63-
self.run()
63+
if self.__run_set:
64+
self.run()
6465
self.assertion()
6566
if self.__after_set and self.after is not None:
6667
self.after()
@@ -76,10 +77,6 @@ def execute_test(self):
7677
def is_valid(self):
7778
is_valid = True
7879

79-
if self.run is None:
80-
self.__add_message_to_error(self.ERROR_MESSAGE_RUN_MISSING)
81-
is_valid = False
82-
8380
if self.assertion is None:
8481
self.__add_message_to_error(self.ERROR_MESSAGE_ASSERTION_MISSING)
8582
is_valid = False

tests/runtime/test_fixtureloader.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,29 @@ def test__load_fixture__two_assertion_one_run_method__adds_two_testclass_to_dict
134134
__assert_test_case_from_dict(loaded_fixture, test_name_1, True, False, False, True)
135135
__assert_test_case_from_dict(loaded_fixture, test_name_2, True, True, False, True)
136136

137+
def test__load_fixture__three_assertion_methods__adds_three_testclass_to_dictionary():
138+
# Arrange
139+
test_name_1 = "fred"
140+
test_name_2 = "hank"
141+
test_name_3 = "bert"
142+
new_class = TestNutterFixtureBuilder() \
143+
.with_name("MyClass") \
144+
.with_assertion(test_name_1) \
145+
.with_assertion(test_name_2) \
146+
.with_assertion(test_name_3) \
147+
.build()
148+
149+
loader = FixtureLoader()
150+
151+
# Act
152+
loaded_fixture = loader.load_fixture(new_class())
153+
154+
# Assert
155+
assert len(loaded_fixture) == 3
156+
__assert_test_case_from_dict(loaded_fixture, test_name_1, True, True, False, True)
157+
__assert_test_case_from_dict(loaded_fixture, test_name_2, True, True, False, True)
158+
__assert_test_case_from_dict(loaded_fixture, test_name_3, True, True, False, True)
159+
137160
def test__load_fixture__three_with_all_methods__adds_three_testclass_to_dictionary():
138161
# Arrange
139162
test_name_1 = "fred"
@@ -166,7 +189,6 @@ def test__load_fixture__three_with_all_methods__adds_three_testclass_to_dictiona
166189
__assert_test_case_from_dict(loaded_fixture, test_name_2, False, False, False, False)
167190
__assert_test_case_from_dict(loaded_fixture, test_name_3, False, False, False, False)
168191

169-
170192
def __assert_test_case_from_dict(test_case_dict, expected_name, before_none, run_none, assertion_none, after_none):
171193
assert expected_name in test_case_dict
172194

tests/runtime/test_nutterfixture.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
from runtime.nutterfixture import NutterFixture, tag, InvalidTestFixtureException
88
from runtime.testcase import TestCase
9+
from runtime.fixtureloader import FixtureLoader
910
from common.testresult import TestResult, TestResults
1011
from tests.runtime.testnutterfixturebuilder import TestNutterFixtureBuilder
1112
from common.apiclientresults import ExecuteNotebookResult
@@ -228,6 +229,15 @@ def test__execute_tests__two_test_cases__returns_test_results_with_2_test_result
228229
# Assert
229230
assert len(result.test_results.results) == 2
230231

232+
def test__execute_tests__test_names_not_in_order_in_class__tests_executed_in_alphabetical_order():
233+
# Arrange
234+
fix = OutOfOrderTestFixture()
235+
236+
# Act
237+
fix.execute_tests()
238+
239+
# Assert
240+
assert '1wxyz' == fix.get_method_order()
231241

232242
def test__run_test_method__has_list_tag_decorator__list_set_on_method():
233243
# Arrange
@@ -296,7 +306,8 @@ def assertion_test(self):
296306

297307
def __get_test_case(name, setrun, setassert):
298308
tc = TestCase(name)
299-
tc.set_run(setrun)
309+
if setrun != None:
310+
tc.set_run(setrun)
300311
tc.set_assertion(setassert)
301312

302313
return tc
@@ -338,3 +349,30 @@ def run_test_with_valid_decorator(self):
338349
def run_test_with_invalid_decorator(self):
339350
pass
340351

352+
class OutOfOrderTestFixture(NutterFixture):
353+
def __init__(self):
354+
super(OutOfOrderTestFixture, self).__init__()
355+
self.__method_order = ''
356+
357+
def assertion_y(self):
358+
self.__method_order += 'y'
359+
assert 1 == 1
360+
361+
def assertion_z(self):
362+
self.__method_order += 'z'
363+
assert 1 == 1
364+
365+
def assertion_1(self):
366+
self.__method_order += '1'
367+
assert 1 == 1
368+
369+
def assertion_w(self):
370+
self.__method_order += 'w'
371+
assert 1 == 1
372+
373+
def assertion_x(self):
374+
self.__method_order += 'x'
375+
assert 1 == 1
376+
377+
def get_method_order(self):
378+
return self.__method_order

tests/runtime/test_nutterfixure_fullroundtriptests.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,30 @@ def test__execute_tests__one_test_case_with_all_methods__all_methods_called(mock
154154
test_fixture.after_test.assert_called_once_with()
155155
test_fixture.after_all.assert_called_once_with()
156156

157+
def test__execute_tests__one_beforeall_2_assertions__all_methods_called(mocker):
158+
# Arrange
159+
test_name_1 = "test"
160+
test_name_2 = "test2"
161+
162+
test_fixture = TestNutterFixtureBuilder() \
163+
.with_name("MyClass") \
164+
.with_before_all() \
165+
.with_assertion(test_name_1) \
166+
.with_assertion(test_name_2) \
167+
.build()
168+
169+
mocker.patch.object(test_fixture, 'before_all')
170+
mocker.patch.object(test_fixture, 'assertion_test')
171+
mocker.patch.object(test_fixture, 'assertion_test2')
172+
173+
# Act
174+
result = test_fixture().execute_tests()
175+
176+
# Assert
177+
test_fixture.before_all.assert_called_once_with()
178+
test_fixture.assertion_test.assert_called_once_with()
179+
test_fixture.assertion_test.assert_called_once_with()
180+
157181
def __item_in_list_equalto(list, expected_item):
158182
for item in list:
159183
if (item == expected_item):

tests/runtime/test_testcase.py

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from runtime.nutterfixture import tag
1111
from runtime.testcase import TestCase, NoTestCasesFoundError
1212

13-
def test__isvalid_rundoesntexist_returnsfalse():
13+
def test__isvalid_assertionexistsrundoesntexist_returnstrue():
1414
# Arrange
1515
tc = TestCase("Test Name")
1616
fixture = TestFixture()
@@ -21,14 +21,16 @@ def test__isvalid_rundoesntexist_returnsfalse():
2121
isvalid = tc.is_valid()
2222

2323
# Assert
24-
assert False == isvalid
24+
assert True == isvalid
2525

2626
def test__isvalid_assertiondoesntexist_returnsfalse():
2727
# Arrange
2828
tc = TestCase("Test Name")
2929
fixture = TestFixture()
3030

31+
tc.set_before(fixture.before_test)
3132
tc.set_run(fixture.run_test)
33+
tc.set_after(fixture.after_test)
3234

3335
# Act
3436
isvalid = tc.is_valid()
@@ -50,21 +52,6 @@ def test__isvalid_runandassertionexist_returnstrue():
5052
# Assert
5153
assert True == isvalid
5254

53-
def test__getinvalidmessage_rundoesntexist_returnsrunerrormessage():
54-
# Arrange
55-
tc = TestCase("Test Name")
56-
fixture = TestFixture()
57-
58-
tc.set_assertion(fixture.assertion_test)
59-
60-
expected_message = tc.ERROR_MESSAGE_RUN_MISSING
61-
62-
# Act
63-
invalid_message = tc.get_invalid_message()
64-
65-
# Assert
66-
assert expected_message == invalid_message
67-
6855
def test__getinvalidmessage_assertiondoesntexist_returnsassertionerrormessage():
6956
# Arrange
7057
tc = TestCase("Test Name")
@@ -80,21 +67,6 @@ def test__getinvalidmessage_assertiondoesntexist_returnsassertionerrormessage():
8067
# Assert
8168
assert expected_message == invalid_message
8269

83-
def test__getinvalidmessage_runandassertiondontexist_returnsrunandassertionerrormessage():
84-
# Arrange
85-
tc = TestCase("Test Name")
86-
fixture = TestFixture()
87-
88-
# Act
89-
invalid_message = tc.get_invalid_message()
90-
91-
# Assert
92-
assertion_message_exists = tc.ERROR_MESSAGE_ASSERTION_MISSING in invalid_message
93-
run_message_exists = tc.ERROR_MESSAGE_RUN_MISSING in invalid_message
94-
95-
assert assertion_message_exists == True
96-
assert run_message_exists == True
97-
9870
def test__set_run__function_passed__sets_run_function():
9971
# Arrange
10072
tc = TestCase("Test Name")
@@ -168,6 +140,33 @@ def test__execute_test__before_not_set__does_not_call_before(mocker):
168140
# Assert
169141
tc.before.assert_not_called()
170142

143+
def test__execute_test__run_set__calls_run(mocker):
144+
# Arrange
145+
tc = TestCase("TestName")
146+
147+
tc.set_run(lambda: 1 == 1)
148+
tc.set_assertion(lambda: 1 == 1)
149+
mocker.patch.object(tc, 'run')
150+
151+
# Act
152+
test_result = tc.execute_test()
153+
154+
# Assert
155+
tc.run.assert_called_once_with()
156+
157+
def test__execute_test__run_not_set__does_not_call_run(mocker):
158+
# Arrange
159+
tc = TestCase("TestName")
160+
161+
tc.set_assertion(lambda: 1 == 1)
162+
mocker.patch.object(tc, 'run')
163+
164+
# Act
165+
test_result = tc.execute_test()
166+
167+
# Assert
168+
tc.run.assert_not_called()
169+
171170
def test__execute_test__after_set__calls_after(mocker):
172171
# Arrange
173172
tc = TestCase("TestName")

0 commit comments

Comments
 (0)