More Advanced Tests

Specifying Return Values

Previously we have specified how a Fake object named "sock" should be used by our code.

When we say s.sock.send(b'the data') we express the expectation that the code under test will call the .send() method with exactly one argument, whose value should equal exactly b'the data'.

When the code does this with "sock"’s .send() method, however, what value is returned by method call?

The answer in this case is None - but Testix also allows us to define this return value. This is useful when you want to test thing related to what function calls on Fake objects return, e.g. thing about testing some code that receives data on one socket, and sends the length of said data to another socket.

We therefore expect that there will be a .recv() call on one socket which returns some data, this data in turn is converted to a number (its length), which is then encoded and sent on the outgoing socket.

Here’s how to test this in Testix

 1from testix import *
 2import forwarder
 3
 4def test_forward_data_lengths():
 5    tested = forwarder.Forwarder()
 6    incoming = Fake('incoming_socket')
 7    outgoing = Fake('outgoing_socket')
 8    with Scenario() as s:
 9        # we'll require that the length of the data is sent, along with a ' ' separator
10        s.incoming_socket.recv(4096)  >>  b'some data'
11        s.outgoing_socket.send(b'9 ')
12        s.incoming_socket.recv(4096)  >>  b'other data'
13        s.outgoing_socket.send(b'10 ')
14        s.incoming_socket.recv(4096)  >>  b'even more data'
15        s.outgoing_socket.send(b'14 ')
16
17        tested.forward_once(incoming, outgoing)
18        tested.forward_once(incoming, outgoing)
19        tested.forward_once(incoming, outgoing)

We see here a pattern which is common with Testix - specifying an entire scenario of what should happen, then making it happen by calling the code under test.

Here’s another version of the same test

 1from testix import *
 2import forwarder
 3
 4def test_forward_data_lengths():
 5    tested = forwarder.Forwarder()
 6    incoming = Fake('incoming_socket')
 7    outgoing = Fake('outgoing_socket')
 8    with Scenario() as s:
 9        # we'll require that the length of the data is sent, along with a ' ' separator
10        s.incoming_socket.recv(4096)  >>  b'some data'
11        s.outgoing_socket.send(b'9 ')
12        tested.forward_once(incoming, outgoing)
13
14        s.incoming_socket.recv(4096)  >>  b'other data'
15        s.outgoing_socket.send(b'10 ')
16        tested.forward_once(incoming, outgoing)
17
18        s.incoming_socket.recv(4096)  >>  b'even more data'
19        s.outgoing_socket.send(b'14 ')
20        tested.forward_once(incoming, outgoing)

You should use the style that makes the test most readable to you.

For later reference, here’s the code that passes this test:

class Forwarder:
    def forward_once(self, read_from, write_to):
        data = read_from.recv(4096)
        binary = bytes(f'{len(data)} ', 'latin-1')
        write_to.send(binary)

Exactness

What happens if we now change the code above to read

1class Forwarder:
2    def forward_once(self, read_from, write_to):
3        data = read_from.recv(4096)
4        binary = bytes(f'{len(data)} ', 'latin-1')
5        write_to.send(binary)
6        write_to.close()

That is, we decided we want to close the outgoing socket for some reason.

$ python -m pytest -v docs/tutorial/other_tests/more_advanced/2/test_forward_lengths.py

 ...


     def _fail_py_test( exceptionFactory, message ):
 >       return pytest.fail( message )
 E       Failed:
 E       testix: ExpectationException
 E       testix details:
 E       === Scenario (no title) ===
 E        expected: incoming_socket.recv(4096)
 E        actual  : outgoing_socket.close()
 E       === OFFENDING LINE ===
 E        write_to.close() (/home/yoav/work/testix/docs/tutorial/other_tests/more_advanced/2/forwarder.py:6)
 E       === FURTHER EXPECTATIONS (showing at most 10 out of 3) ===
 E        outgoing_socket.send(b'10 ')
 E        incoming_socket.recv(4096)
 E        outgoing_socket.send(b'14 ')
 E       === END ===

As you can see, the test fails, and the new .close() is now, in Testix jargon, the “offending line”.

This is because Testix expectations are asserted in an exact manner - we define exactly what we want, no more - no less.

This makes Testix very conducive to Test Driven Development - if you change the code before changing the test - it will probably result in failures. When approaching adding new features - start with defining a test for them.

We’ll discuss exactness some more next.