DEV Community

Ninad Mhatre
Ninad Mhatre

Posted on

software testing, assertions

I work on a testing framework which validates application by sending and receiving messages with the proprietary protocol. And certainly, there are a lot of assertions with most of them checking the value of the field, some checks the type of message received and some ordering of messages. All of these assertions are grouped by functionality in multiple functions across multiple files. Having a lot of assertions is not a problem but not showing a clear message is certainly a problem. Imaging test being failed with something like this

... long stack trace ...
AssetionError: True != False
Enter fullscreen mode Exit fullscreen mode

looking at it just gives a basic idea that something should have been True but was not! If you read stack trace you can figure out where the assertion failed, but there is a big "if you read" Nobody reads it! Then we get a lot of links to failed jobs with a sweet message, "tests are failing".

This is due to having lot of bare asserts, when we started developing the tests our the main focus was converting manual tests to automated and that's why we have lot of asserts lying around in our code base.

The second pain point was not that big deal, but surely good to address is, when we check new message structure and values, its fails one by one. If message has 20 fileds then you can imagine how long it will take to figure out all wrongly populated fields. I wanted to report all failures at once so that I can ask a developer to take a look at all in one go. There is already a solution to this, delayed_assert module in python, but that is a wrapper around assert which stores stack trace and dumps in the end and it also meant that i need to replace all assets.

I decided to solve both the problems in one go, I searched and found few assertion libraries in python, one particular I liked was assertpy because it has clean API and it has assert_warn function which would just report the errors (without stack trace) but won't stop the execution. My idea was, write a wrapper which would return assert_that or assert_warn depending upon a certain condition.

# some wrapper file
import os
from assertpy import assert_that, assert_warn

def get_assertor():
   if bool(os.getenv("ERRORS_AS_WARN", False)):
       return assert_warn
   else:
       return assert_that

assert_that = get_assertor()
Enter fullscreen mode Exit fullscreen mode

also the API os assertpy is just brilliant

from assertpy import assert_that
from nose.tools import assert_equal

# not very easy to understand
assert_equal(1 + 3, 2, "4 is not equal to 2")

# its like reading a sentance
assert_that(1 + 3, "addition").is_equal_to(2)
Enter fullscreen mode Exit fullscreen mode

I decided I will use assertpy but then I started implementing and I realized,

1) we only use a handful of assertions and regular ones that we use are not in assertpy

2) though assertion methods can be easily plugged in assertpy, requirement of such function is the first argument named as self, which felt bit odd

def is_in_between(self, other): 
   ...
Enter fullscreen mode Exit fullscreen mode

so I checked the code and realized that for every assert_that, a new instance of the class is created and extensions are added on every initialization, I thought its extra work for little gain.

3) my dynamic error to the warning was working but it was printing the wrong location of the source as it was looking for assert_warn in stack trace but I was referring it to as assert_that, in short it was only working partially.

Adding PR to assertpy seemed worthless as all solutions felt like a hack and then I decided to write one which is based on assertpy but has very basic functionality but easy to extend.

I created simple-assertions, after thinking many times whether to re-invent the wheel or not...

how I solved the above issues?

1) I created a class with a handful of common assertions, the class was picked as you can easily inherit to extend the functionality.

from simple_assertions import SimpleAssertions
from typing import Union

class YourAssertions(SimpleAssertions):
    def __init__(self, as_warn=False, logger=None):
        super().__init__(as_warn, logger)

    def is_greater_than(self, other: Union[int, float]):
        if self.val_to_chk.val < other:
            self.error(self.generate_err_msg(other, "to be greater than"))
        return self


class YourTestClass:
    def __init__(self):
        self.assert_that = YourAssertions().assert_that

    def test_something(self):
        self.assert_that(4 + 10).is_greater_than(10).is_equal_to(14)
        self.assert_that(1).is_instance_of(int)
        self.assert_that(3, "lucky_num", as_warn=True).is_equal_to(4)      
Enter fullscreen mode Exit fullscreen mode

2) With composition, an instance of assertion class can be created and then that single instance can be reused. IDE can also help me code completion.

class MyTests(unittest.Testcase):
    def setUp(self):
        self.assert_that = SimpleAssert().assert_that
Enter fullscreen mode Exit fullscreen mode

3) I changed the API of assert_that and instead of 2 functions, 1 for error and 1 for the warning, I added an optional parameter to create warnings, also added an environment variable which if set will convert all error to warnings.

Use of environment variable to change the behaviour is topic for discussion but for my needs, it was a perfect solution!


# instead of, assert_warn(10).is_equal_to(9)

assert_that(10, as_warn=True).is_equal_to(9)

Enter fullscreen mode Exit fullscreen mode

I am planning to add a few more assertions so that it can be used without need to adding anything. If you want to give it a try, all you have to do is

pip install simple-assertions
Enter fullscreen mode Exit fullscreen mode

and you can check the source here please check here https://github.com/ninadmhatre/simple-assertions

Top comments (0)