Package: django-perf-rec
“Keep detailed records of the performance of your Django code.”django-perf-recis like Django’sassertNumQuerieson steroids. It lets
you track the individual queries and cache operations that occur in your code.
Use it in your tests like so:deftest_home(self):withdjango_perf_rec.record():self.client.get("/")It then stores a YAML file alongside the test file that tracks the queries and
operations, looking something like:MyTests.test_home:-cache|get:home_data.user_id.#-db:'SELECT...FROMmyapp_tableWHERE(myapp_table.id=#)'-db:'SELECT...FROMmyapp_tableWHERE(myapp_table.id=#)'When the test is run again, the new record will be compared with the one in the
YAML file. If they are different, an assertion failure will be raised, failing
the test. Magic!The queries and keys are ‘fingerprinted’, replacing information that seems
variable with#and…. This is done to avoid spurious failures when e.g.
primary keys are different, random data is used, new columns are added to
tables, etc.If you check the YAML file in along with your tests, you’ll have unbreakable
performance with much better information about any regressions compared toassertNumQueries. If you are fine with the changes from a failing test,
just remove the file and rerun the test to regenerate it.For more information, see ourintroductory blog
postthat
says a little more about why we made it.InstallationUsepip:python-mpipinstalldjango-perf-recRequirementsPython 3.8 to 3.12 supported.Django 3.2 to 5.0 supported.Are your tests slow?Check out my bookSpeed Up Your Django Testswhich covers loads of ways to write faster, more accurate tests.APIrecord(record_name: str | None=None, path: str | None=None, capture_traceback:callable[[Operation],bool] | None=None, capture_operation:callable[[Operation],bool] | None=None)Return a context manager that will be used for a single performance test.The arguments must be passed as keyword arguments.pathis the path to a directory or file in which to store the record. If it
ends with'/', or is left asNone, the filename will be automatically
determined by looking at the filename the calling code is in and replacing the.py[c]extension with.perf.yml. If it points to a directory that
doesn’t exist, that directory will be created.record_nameis the name of the record inside the performance file to use.
If left asNone, the code assumes you are inside a DjangoTestCaseand
uses magic stack inspection to find that test case, and uses a name based upon
the test case name + the test method name + an optional counter if you invokerecord()multiple times inside the same test method.Whilst open, the context manager tracks all DB queries on all connections, and
all cache operations on all defined caches. It names the connection/cache in
the tracked operation it uses, except from for thedefaultone.When the context manager exits, it will use the list of operations it has
gathered. If the relevant file specified usingpathdoesn’t exist, or
doesn’t contain data for the specificrecord_name, it will be created and
saved and the test will pass with no assertions. However if the recorddoesexist inside the file, the collected record will be compared with the original
one, and if different, anAssertionErrorwill be raised. When running on
pytest, this will use its fancy assertion rewriting; in other test runners/uses
the full diff will be attached to the message.Example:importdjango_perf_recfromapp.modelsimportAuthorclassAuthorPerformanceTests(TestCase):deftest_special_method(self):withdjango_perf_rec.record():list(Author.objects.special_method())capture_traceback, if notNone, should be a function that takes one
argument, the given DB or cache operation, and returns aboolindicating
if a traceback should be captured for the operation (by default, they are not).
Capturing tracebacks allows fine-grained debugging of code paths causing the
operations. Be aware that records differing only by the presence of tracebacks
will not match and cause anAssertionErrorto be raised, so it’s not
normally suitable to permanently record the tracebacks.For example, if you wanted to know what code paths query the tablemy_table, you could use acapture_tracebackfunction like so:defdebug_sql_query(operation):return"my_tables"inoperation.querydeftest_special_method(self):withdjango_perf_rec.record(capture_traceback=debug_sql_query):list(Author.objects.special_method())The performance record here would include a standard Python traceback attached
to each SQL query containing “my_table”.capture_operation, if notNone, should be a function that takes one
argument, the given DB or cache operation, and returns aboolindicating if
the operation should be recorded at all (by default, all operations are
recorded). Not capturing some operations allows for hiding some code paths to be
ignored in your tests, such as for ignoring database queries that would be
replaced by an external service in production.For example, if you knew that in testing all queries to some table would be
replaced in production with something else you could use acapture_operationfunction like so:defhide_my_tables(operation):return"my_tables"inoperation.querydeftest_special_function(self):withdjango_perf_rec.record(capture_operation=hide_my_tables):list(Author.objects.all())TestCaseMixinA mixin class to be added to your customTestCasesubclass so you can usedjango-perf-recacross your codebase without needing to import it in each
individual test file. It adds one method,record_performance(), whose
signature is the same asrecord()above.Example:# yplan/test.pyfromdjango.testimportTestCaseasOrigTestCasefromdjango_perf_recimportTestCaseMixinclassTestCase(TestCaseMixin,OrigTestCase):pass# app/tests/models/test_author.pyfromapp.modelsimportAuthorfromyplan.testimportTestCaseclassAuthorPerformanceTests(TestCase):deftest_special_method(self):withself.record_performance():list(Author.objects.special_method())get_perf_path(file_path)Encapsulates the logic used inrecord()to formpathfrom the path of
the file containing the currently running test, mostly swapping ‘.py’ or ‘.pyc’
for ‘.perf.yml’. You might want to use this when callingrecord()from
somewhere other than inside a test (which causes the automatic inspection to
fail), to match the same filename.get_record_name(test_name, class_name=None)Encapsulates the logic used inrecord()to form arecord_namefrom
details of the currently running test. You might want to use this when callingrecord()from somewhere other than inside a test (which causes the
automatic inspection to fail), to match the samerecord_name.SettingsBehaviour can be customized with a dictionary calledPERF_RECin your
Django settings, for example:PERF_REC={"MODE":"once",}The possible keys to this dictionary are explained below.HIDE_COLUMNSTheHIDE_COLUMNSsetting may be used to change the waydjango-perf-recsimplifies SQL in the recording files it makes. It takes a boolean:True(default) causes column lists in queries to be collapsed, e.g.SELECT a, b, c FROM tbecomesSELECT ... FROM t. This is useful
because selected columns often don’t affect query time in typical
Django applications, it makes the records easier to read, and they then don’t
need updating every time model fields are changed.Falsestops the collapsing behaviour, causing all the columns to be
output in the files.MODETheMODEsetting may be used to change the waydjango-perf-recbehaves
when a performance record does not exist during a test run.'once'(default) creates missing records silently.'none'raisesAssertionErrorwhen a record does not exist. You
probably want to use this mode in CI, to ensure new tests fail if their
corresponding performance records were not committed.'all'creates missing records and then raisesAssertionError.'overwrite'creates or updates records silently.Usage in PytestIf you’re using Pytest, you might want to callrecord()from within a
Pytest fixture and have it automatically apply to all your tests. We have an
example of this, see the filetest_pytest_fixture_usage.pyin the test suite.