Production code isn't the only thing that evolves

We keep saying that unit tests are as important as production code. And if we are careful enough with production code as to create the best possible abstraction, why wouldn't we do the same for unit tests?

If the code for unit tests is as important as the main code, then it's definitely wise to design it with extensibility in mind and make it as maintainable as possible. After all, this is the code that will have to be maintained by an engineer other than its original author, so it has to be readable.

The reason why we pay so much attention to make the code's flexibility is that we know requirements change and evolve over time, and eventually as domain business rules change, our code will have to change as well to support these new requirements. Since the production code changed to support new requirements, in turn, the testing code will have to change as well to support the newer version of the production code.

In one of the first examples we used, we created a series of tests for the merge request object, trying different combinations and checking the status at which the merge request was left. This is a good first approach, but we can do better than that.

Once we understand the problem better, we can start creating better abstractions. With this, the first idea that comes to mind is that we can create a higher-level abstraction that checks for particular conditions. For example, if we have an object that is a test suite that specifically targets the MergeRequest class, we know its functionality will be limited to the behavior of this class (because it should comply to the SRP), and therefore we could create specific testing methods on this testing class. These will only make sense for this class, but that will be helpful in reducing a lot of boilerplate code.

Instead of repeating assertions that follow the exact same structure, we can create a method that encapsulates this and reuse it across all of the tests:

class TestMergeRequestStatus(unittest.TestCase):
def setUp(self):
self.merge_request = MergeRequest()

def assert_rejected(self):
self.assertEqual(
self.merge_request.status, MergeRequestStatus.REJECTED
)

def assert_pending(self):
self.assertEqual(
self.merge_request.status, MergeRequestStatus.PENDING
)

def assert_approved(self):
self.assertEqual(
self.merge_request.status, MergeRequestStatus.APPROVED
)

def test_simple_rejected(self):
self.merge_request.downvote("maintainer")
self.assert_rejected()

def test_just_created_is_pending(self):
self.assert_pending()

If something changes with how we check the status of a merge request (or let's say we want to add extra checks), there is only one place (the assert_approved() method) that will have to be modified. More importantly, by creating these higher-level abstractions, the code that started as merely unit tests starts to evolve into what could end up being a testing framework with its own API or domain language, making testing more declarative.

..................Content has been hidden....................

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