Parametrized tests

Now, we would like to test how the threshold acceptance for the merge request works, just by providing data samples of what the context looks like without needing the entire MergeRequest object. We want to test the part of the status property that is after the line that checks if it's closed, but independently.

The best way to achieve this is to separate that component into another class, use composition, and then move on to test this new abstraction with its own test suite:

class AcceptanceThreshold:
def __init__(self, merge_request_context: dict) -> None:
self._context = merge_request_context

def status(self):
if self._context["downvotes"]:
return MergeRequestStatus.REJECTED
elif len(self._context["upvotes"]) >= 2:
return MergeRequestStatus.APPROVED
return MergeRequestStatus.PENDING


class MergeRequest:
...
@property
def status(self):
if self._status == MergeRequestStatus.CLOSED:
return self._status

return AcceptanceThreshold(self._context).status()

With these changes, we can run the tests again and verify that they pass, meaning that this small refactor didn't break anything of the current functionality (unit tests ensure regression). With this, we can proceed with our goal to write tests that are specific to the new class:

class TestAcceptanceThreshold(unittest.TestCase):
def setUp(self):
self.fixture_data = (
(
{"downvotes": set(), "upvotes": set()},
MergeRequestStatus.PENDING
),
(
{"downvotes": set(), "upvotes": {"dev1"}},
MergeRequestStatus.PENDING,
),
(
{"downvotes": "dev1", "upvotes": set()},
MergeRequestStatus.REJECTED
),
(
{"downvotes": set(), "upvotes": {"dev1", "dev2"}},
MergeRequestStatus.APPROVED
),
)

def test_status_resolution(self):
for context, expected in self.fixture_data:
with self.subTest(context=context):
status = AcceptanceThreshold(context).status()
self.assertEqual(status, expected)

Here, in the setUp() method, we define the data fixture to be used throughout the tests. In this case, it's not actually needed, because we could have put it directly on the method, but if we expect to run some code before any test is executed, this is the place to write it, because this method is called once before every test is run.

By writing this new version of the code, the parameters under the code being tested are  clearer and more compact, and at each case, it will report the results.

To simulate that we're running all of the parameters, the test iterates over all the data, and exercises the code with each instance. One interesting helper here is the use of subTest, which in this case we use to mark the test condition being called. If one of these iterations failed, unittest would report it with the corresponding value of the variables that were passed to the subTest (in this case, it was named context, but any series of keyword arguments would work just the same). For example, one error occurrence might look like this:

FAIL: (context={'downvotes': set(), 'upvotes': {'dev1', 'dev2'}})
----------------------------------------------------------------------
Traceback (most recent call last):
File "" test_status_resolution
self.assertEqual(status, expected)
AssertionError: <MergeRequestStatus.APPROVED: 'approved'> != <MergeRequestStatus.REJECTED: 'rejected'>
If you choose to parameterize tests, try to provide the context of each instance of the parameters with as much information as possible to make debugging easier.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.168.83