A first quick start at some tests may help me get off the dime here. Can I trick myself?

Following on the heels of the preceding article, I propose to get a couple of tests in place, just so that next time it won’t seem so daunting to create tests. They won’t be great, but they will be a foothold for better tests.

What Could I Test?

I can certainly test hookup:

class TestLibClient:
    def test_hookup(self):
        assert False

One nice thing about the hookup test is that it’s easy, independent of what I’m testing, and it could be set up as a snippet if I wanted to. But now we have tests … they’re just not very good.

Now I’ll scan the ‘libclient.py’ file to see if there is anything that I could test right now with a simple call.

These two methods seem like candidates:

    def _json_encode(self, obj, encoding):
        return json.dumps(obj, ensure_ascii=False).encode(encoding)

    def _json_decode(self, json_bytes, encoding):
        tiow = io.TextIOWrapper(
            io.BytesIO(json_bytes), encoding=encoding, newline=""
        )
        obj = json.load(tiow)
        tiow.close()
        return obj

Neither of these references self. PyCharm points out that they could be static. This is certainly a hint that they are independently testable, but also that possibly they belong on a different class.

I honestly have no clue what io.TextIOWrapper is about but it seems very likely that if we were to put an object into the encode and then take that and put it into the decode, we should get the same object back.

I think we’re passing dictionaries in here. I’ll look for a sender.

    def queue_request(self):
        content = self.request["content"]
        content_type = self.request["type"]
        content_encoding = self.request["encoding"]
        if content_type == "text/json":
            req = {
                "content_bytes": self._json_encode(content, content_encoding),
                "content_type": content_type,
                "content_encoding": content_encoding,
            }
        ...

What does the request look like? In app-client, we find:

def create_request(action, value):
    if action == "search":
        return dict(
            type="text/json",
            encoding="utf-8",
            content=dict(action=action, value=value),
        )

Looks like a dictionary containing a dictionary. I think I’ll try something easier:

class TestLibClient:
    def test_encode(self):
        dict = { "a": 1, "b":"two"}
        msg = Message(None, None, None, None)
        result = msg._json_encode(dict, "utf-8")
        assert result == ''

This fails delightfully:

Expected :''
Actual   :b'{"a": 1, "b": "two"}'

Looks like JSON to me. But we’re just going to decode it anyway:

class TestLibClient:
    def test_encode_decode(self):
        dict = { "a": 1, "b":"two"}
        msg = Message(None, None, None, None)
        result = msg._json_encode(dict, "utf-8")
        new_dict = msg._json_decode(result, "utf-8")
        assert new_dict == dict

This test passes. We are not surprised. It’s not testing much more than Python’s json library and whatever that TextIOWrapper thing is doing. I’d like of like to know what that is doing.

I was starting to write a test for it, and got this far:

    def test_textiowrapper(self):
        dict = { "a": 1, "b":"two"}
        msg = Message(None, None, None, None)
        result = msg._json_encode(dict, "utf-8")
        unwrapped = io.TextIOWrapper(
            io.BytesIO(result), encoding="utf-8", newline=""
        )
        assert

When a click on the json.load back in the real code told me that json.load wants a file-like object from which to read its bytes. So I don’t feel the need for that test after all. Remove it. The TextIOWrapper is like a StringReader, I gather.

Well, we don’t have much in the way of testing yet. I would really like to have a bit more before I quit for the morning.

This method seems as if it would be possible to test:

    def _create_message(
        self, *, content_bytes, content_type, content_encoding
    ):
        jsonheader = {
            "byteorder": sys.byteorder,
            "content-type": content_type,
            "content-encoding": content_encoding,
            "content-length": len(content_bytes),
        }
        jsonheader_bytes = self._json_encode(jsonheader, "utf-8")
        message_hdr = struct.pack(">H", len(jsonheader_bytes))
        message = message_hdr + jsonheader_bytes + content_bytes
        return message

I’d like to see what we get from that, and we can call it with suitable parameters, I think. We’ll try.

    def test_create_message(self):
        msg = Message(None, None, None, None)
        content = b'INFORMATION'
        content_dict = {
            'content_bytes': content,
            'content_type': 'text/json',
            'content_encoding': 'utf-8'
        }
        result = msg._create_message(**content_dict)
        assert result == b''

This fails more or less as I had hoped:

(b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding'
 b'": "utf-8", "content-length": 11}INFORMATION') != b''

That seems to me to be malformed. In particular, the end of the first line there has that weird starts with quote ends with prime thing, and then another byte string picking up the quote and the rest of the line. Is that just an artifact of the print? And is 0x00g{ a valid number? I would not have thought so.

Note
I confused myself because PyCharm has folded the literal for me, and because struct.pack has done some kind of odd thing with the length, but it comes back ok after the unpack.

I’m pretty sure that my input is invalid, but let’s put in some prints to see what’s up.

A bit of printing tells me that whatever that 0x00g means, it will unpack as 103, which is the length that went in. I recast the test a bit:

    def test_create_message(self):
        msg = Message(None, None, None, None)
        content = b'INFORMATION'
        content_dict = {
            'content_bytes': content,
            'content_type': 'text/json',
            'content_encoding': 'utf-8'
        }
        result = msg._create_message(**content_dict)
        length = struct.unpack(">H", result[:2])
        assert length == (103,)
        result = result[2:]
        assert result == b'{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 11}INFORMATION'

I made one big long b-string and the test passes, telling me that the odd break in the print is just a line folding trick. I can probably change my test to be like this:

    def test_create_message(self):
        msg = Message(None, None, None, None)
        content = b'INFORMATION'
        content_dict = {
            'content_bytes': content,
            'content_type': 'text/json',
            'content_encoding': 'utf-8'
        }
        result = msg._create_message(**content_dict)
        length = struct.unpack(">H", result[:2])
        assert length == (103,)
        result = result[2:]
        assert result == (b'{"byteorder": "little", "content-type": "text/json",'
                          b' "content-encoding": "utf-8", "content-length": 11}'
                          b'INFORMATION')

Sure enough, it still passes! We have learned a few things:

  • struct.pack and unpack returns some really odd stuff. I don’t know how it converted 103 into 00g but it seems to be ok.

  • We can fold strings in python by putting a close quote, return, open quote. PyCharm does it automatically if you put an enter into a string. And it seems that it did it on the output, which confused me at first.

  • We are probably pretty close to the form of the actual results from the object, with a large caveat around the final content, which might be more structured than we see here, or might just be a string. We could look at the server “database” to find out, but no.

  • We have identified three methods that don’t really rely on the member variables of the class. These may be candidates for removal.

And we have a foothold for our tests. The tests we have are weak, but they are legitimate tests, and now the initial resistance to setting up a test has been reduced. It will be much easier, tomorrow, to just open up the test file and figure out another test. I always find that that first step is the one that holds me back. And now it’s taken.

I wonder whether I could always create a hookup and some trivial test, enough to get that initial push over with. Might be worth trying. I’m easily tricked.

See you next time!