Package: pytest-tcpclient
pytest-tcpclientis apytestplugin that helps you write better TCP clients.It provides two fixtures,tcpserverandtcpserver_factory.Behind the scenes, thetcpserverfixture creates an in-process TCP server
that listens on a port to which the client can connect and send messages and
from which it can receive replies.Thetcpserverfixture is used to express expectations about what messages the
client sends and also to send replies to it. If any expectation is unfulfilled, the
test will fail with a diagnostic message.ContentsHelloTest FailureSending and receiving length-delimited “frames”DevelopmentHello!Here’s an example of a passing test that uses thetcpserverfixture:importasyncioimportpytest@pytest.mark.asyncio()asyncdeftest_hello(tcpserver):# ==========================================================================# Establish expectations and replies on the server side.## In this case, the client is expected to connect and then send a specific# message. If that occurs, the server will send a reply. It is expected# that the client will then disconnect.tcpserver.expect_connect()tcpserver.expect_bytes(b"Hello, server!")tcpserver.send_bytes(b"Hello, client!")tcpserver.expect_disconnect()# ==========================================================================# Client interacts with server.## It connects, sends the expected message and then receives the reply.# Finally, it disconnects from the server.reader,writer=awaitasyncio.open_connection(None,tcpserver.service_port)writer.write(b"Hello, server!")assertawaitreader.readexactly(14)==b"Hello, client!"writer.close()awaitwriter.wait_closed()# ==========================================================================# Synchronise with server. Test failures, if any, will be reported here. In# this case, there are no failures.awaittcpserver.join()Here’s the result:=============================testsessionstarts==============================platformlinux--Python3.8.10,pytest-7.2.0,pluggy-1.0.0rootdir:/home/anders/src/pytest-tcpclient,configfile:pyproject.tomlplugins:mock-3.10.0,asyncio-0.20.2,cov-4.0.0,tcpclient-0.7.26.dev1+gad4c8b2.d20221115asyncio:mode=strictcollected1itemexamples/test_hello.py.[100%]==============================1passedin0.03s===============================Test FailureThis example demonstrates test failure.It is similar to the previous example except that the client does not send the
expected message. As a result, the server times out while waiting for that
message and the test fails.importasyncioimportpytest@pytest.mark.asyncio()asyncdeftest_expect_bytes_times_out(tcpserver):# --------------------------------------------------------------------------# Server expectations. The server just expects the client to connect, send# a message and then disconnect.tcpserver.expect_connect()tcpserver.expect_bytes(b"Hello, world!")tcpserver.expect_disconnect()# --------------------------------------------------------------------------# The client connects but it does not send the message and it does not close# the connection.reader,writer=awaitasyncio.open_connection(None,tcpserver.service_port)# --------------------------------------------------------------------------# The server will time out waiting for the expected message. The test will# fail with a diagnostic message.awaittcpserver.join()Here’s the result:=============================testsessionstarts==============================platformlinux--Python3.8.10,pytest-7.2.0,pluggy-1.0.0rootdir:/home/anders/src/pytest-tcpclient,configfile:pyproject.tomlplugins:mock-3.10.0,asyncio-0.20.2,cov-4.0.0,tcpclient-0.7.26.dev1+gad4c8b2.d20221115asyncio:mode=strictcollected1itemexamples/test_expect_bytes_times_out.pyF[100%]===================================FAILURES===================================_________________________test_expect_bytes_times_out__________________________tcpserver=<pytest_tcpclient.plugin.MockTcpServerobjectat0x7fda575068b0>@pytest.mark.asyncio()asyncdeftest_expect_bytes_times_out(tcpserver):# --------------------------------------------------------------------------# Server expectations. The server just expects the client to connect, send# a message and then disconnect.tcpserver.expect_connect()tcpserver.expect_bytes(b"Hello, world!")tcpserver.expect_disconnect()# --------------------------------------------------------------------------# The client connects but it does not send the message and it does not close# the connection.reader,writer=awaitasyncio.open_connection(None,tcpserver.service_port)# --------------------------------------------------------------------------# The server will time out waiting for the expected message. The test will# fail with a diagnostic message.>awaittcpserver.join()EFailed:Timedoutwaitingforb'Hello, world!'examples/test_expect_bytes_times_out.py:26:Failed===========================shorttestsummaryinfo============================FAILEDexamples/test_expect_bytes_times_out.py::test_expect_bytes_times_out==============================1failedin1.04s===============================Sending and receiving length-delimited “frames”A common pattern for TCP communication is to send and receive discrete messages. One
way to represent the boundaries of these messages is to prepend a 4-byte binary
integer in network (big-endian) ordering that is the length of the payload. Inpytest-tcpclientthis convention is called “framing”.Here’s an example of testing that a client sends a frame (expect_frame):importasyncioimportpytestimportstruct@pytest.mark.asyncio()asyncdeftest_expect_frame_success(tcpserver):tcpserver.expect_connect()tcpserver.expect_frame(b"Goodbye, world")reader,writer=awaitasyncio.open_connection(None,tcpserver.service_port)payload=b"Goodbye, world"# Here's the 4-byte length headerwriter.write(struct.pack(">I",len(payload)))# Here's the payloadwriter.write(payload)# Donewriter.close()awaitwriter.wait_closed()awaittcpserver.join()Here’s the result:=============================testsessionstarts==============================platformlinux--Python3.8.10,pytest-7.2.0,pluggy-1.0.0rootdir:/home/anders/src/pytest-tcpclient,configfile:pyproject.tomlplugins:mock-3.10.0,asyncio-0.20.2,cov-4.0.0,tcpclient-0.7.26.dev1+gad4c8b2.d20221115asyncio:mode=strictcollected1itemexamples/test_expect_frame_success.py.[100%]==============================1passedin0.02s===============================Here’s an example where the server sends a frame (send_frame) and the client
receives it:importasyncioimportpytestimportstruct@pytest.mark.asyncio()asyncdeftest_send_frame_success(tcpserver):tcpserver.expect_connect()reader,writer=awaitasyncio.open_connection(None,tcpserver.service_port)# The server immediately sends a frametcpserver.send_frame(b"Hello")# The client receives the frame. First the header and then the payload.header_bytes=awaitreader.readexactly(4)message_length,=struct.unpack(">I",header_bytes)assertawaitreader.readexactly(message_length)==b"Hello"writer.close()awaitwriter.wait_closed()Here’s the result:=============================testsessionstarts==============================platformlinux--Python3.8.10,pytest-7.2.0,pluggy-1.0.0rootdir:/home/anders/src/pytest-tcpclient,configfile:pyproject.tomlplugins:mock-3.10.0,asyncio-0.20.2,cov-4.0.0,tcpclient-0.7.26.dev1+gad4c8b2.d20221115asyncio:mode=strictcollected1itemexamples/test_send_frame_success.py.[100%]==============================1passedin0.02s===============================DevelopmentIf you want to use a virtual environment, do that first and activate it. You
can use any virtual environment system you like. However, if you want to usevirtualenv(and you already havevirtualenvinstalled) you could do this:$virtualenv-p3.8venvNext, make the project:$makeThat will do the following:Install all the dependenciesRun the testsGenerate a coverage reportFail if the coverage is below 100%Build configurationBuild configuration is mostly stored inpyproject.toml. Idealy, it would all be in
there. However, there are two exceptions.First,setuptoolshas been chosen as the build system. Unfortunately, to installpytest-tcpclientin editable mode, a minimialsetup.pymust be and has been provided.Packages thatpytest-tcpclientrequires to run are listed inpyproject.toml.Packages required for development (testing, coverage and linting) are listed indev_dependencies.txt.toxhas been configured (intox.ini) to install those packages before running
the tests.setuptoolshas been configured to supply the optiondevfor those extra packages.
For example, theMakefilehas the following command to initialise the virtual
environment:$python-mpipinstall-e.[dev]Defaultmaketarget isstyle_and_testThe default target in theMakfileisstyle_and_testwhich first calls
the linter, then runs the tests and, finally, checks that code coverage is 100%toxtoxis only used for CI builds. See.github/workflows/build.yml.makedetects changes to configuration filesIf any of the build system configuration files,makewill reinstall all dependencies.Continuous Integration and DeploymentThere is a workflow (.github/workflows/build.yml) that will build and test pull
requests withtox.There is another workflow (.github/workflows/publish.yml) that is triggered
by the appearance of new version tags on themainbranch. It will
build and test the code and additionally publish the package topypi.