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.