Log-based testing 🪵#

logot makes it easy to test your application is logging correctly:

from logot import Logot, logged

def test_my_app(logot: Logot) -> None:
   app.start()
   logot.wait_for(logged.info("App started"))

Note

These examples all show using logot with pytest. See Using with unittest to learn about about using logot with other testing frameworks.

Why test logging? 🤔#

Good logging ensures your application is debuggable at runtime, but why bother actually testing your logs? After all… surely the worst that can happen is your logs are a bit wonky? 🥴

Sometimes, testing logs is the only reasonable way to known your code has actually run correctly! This is particularly the case in threaded or asynchronous applications where work is carried out at unpredictable times by background workers.

For example, imagine the following code running in a thread:

def poll_daemon(app: App) -> None:
   while not app.stopping:
      sleep(app.poll_interval)
      logger.debug("Poll started")
      try:
         app.data = app.get("http://is-everything-ok.com/")
      except HTTPError:
         logger.exception("Poll error")
      else:
         logger.debug("Poll finished")

Note

It’s certainly possible to rewrite this code in a way that can be tested without logot, but that often makes the code less clear or more verbose. For complex threaded or asynchronous applications, this can quickly become burdensome. 👎

Testing this code with logot is easy!

from logot import Logot, logged

def test_poll_daemon(logot: Logot) -> None:
   app.start_poll()
   for _ in range(3):
      logot.wait_for(logged.info("Poll started"))
      logot.wait_for(logged.info("Poll finished"))

Testing threaded code#

Use Logot.wait_for() to pause your test until the expected logs arrive or the timeout expires:

from logot import Logot, logged

def test_my_app(logot: Logot) -> None:
   app.start()
   logot.wait_for(logged.info("App started"))

Note

Use the timeout argument to Logot.wait_for() to configure how long to wait before the test fails. This can be configured globally with the timeout argument to Logot, defaulting to Logot.DEFAULT_TIMEOUT.

See also

See Log pattern matching for examples of how to wait for logs that may arrive in an unpredictable order.

Testing asynchronous code#

Use Logot.await_for() to pause your test until the expected logs arrive or the timeout expires:

from logot import Logot, logged

async def test_my_app(logot: Logot) -> None:
   app.start()
   await logot.await_for(logged.info("App started"))

Note

Use the timeout argument to Logot.await_for() to configure how long to wait before the test fails. This can be configured globally with the timeout argument to Logot, defaulting to Logot.DEFAULT_TIMEOUT.

See also

See Log pattern matching for examples of how to wait for logs that may arrive in an unpredictable order.

Testing synchronous code#

Use Logot.assert_logged() to fail immediately if the expected logs have not arrived:

from logot import Logot, logged

def test_my_app(logot: Logot) -> None:
   app.run()
   logot.assert_logged(logged.info("App started"))

Note

You can also use Logot.wait_for() to test for expected logs, but since this only fails after a timeout, using Logot.assert_logged() will give more immediate feedback if your test fails.

See also

Use Logot.assert_not_logged() to fail immediately if the expected logs do arrive.

Further reading#

Learn more about logot with the following guides: