内容简介:Whilst writingThis post is my introduction to the problem, the trade-offs with the other libraries, and how my new library,It’s especially common in web projects to have features that rely on changes to the current date and time. For example, you might hav
Whilst writing Speed Up Your Django Tests , I wanted to add a section about mocking the current time. I knew of two libraries such mocking, but I found it hard to pick one to recommend due to the trade-offs in each. So I delayed adding that section and shaved a rather large yak by writing a third library.
This post is my introduction to the problem, the trade-offs with the other libraries, and how my new library, time-machine , tries to solve them. The next version of Speed Up Your Django Tests can now include a section on mocking time.
The Problem
It’s especially common in web projects to have features that rely on changes to the current date and time. For example, you might have a feature where each customer may only make an order once per day.
For testing such features, it’s often necessary to mock the functions that return the current date and time.
In Python’s standard library, these live in the datetime
and time
modules.
There are various ways of doing this mocking: it can be done generically with unittest.mock
, or in a more targeted fashion with a time-mocking library.
Using unittest.mock
works, but it’s often inaccurate each patcher can only mock a single function reference.
This inaccuracy is exacerbated when mocking the current time, as there are many different functions that return the current time, in different formats.
Due to the way Python’s imports work, it takes a lot of mocks to replace every
instance of functions from datetime
and time
in a code path, and is sometimes impossible.
(See: Why Your Mock Doesn’t Work
.)
I know of two existing Python libraries that have tried to provide a better way to mock the current time: freezegun and libfaketime . Let’s look at them now, before I introduce time-machine .
freezegun
freezegun is a very popular library for mocking the current time. It has a great, clear API, and “does what it says on the tin.”
For example, you can write a time-mocking test like so:
import datetime as dt import freezegun @freezegun.freeze_time("1955-11-05 01:22") def test_delorean(): assert dt.date.today().isoformat() == "1955-11-05"
The main drawback is its slow implementation.
It essentially does a find-and-replace mock of all the places that the relevant functions from the datetime
and time
modules have been imported.
This gets around the problems with using unittest.mock
, but it means the time it takes to do the mocking is proportional to the number of loaded modules.
In large projects, this can take a second or two, an impractical overhead for each individual test.
It’s also not a perfect search, since it searches only module-level imports. Such imports are definitely the most common way projects use date and time functions, but they’re not the only way. freezegun won’t find functions that have been “hidden” inside arbitrary objects, such as class-level attributes.
It also can’t affect C extensions that call the standard library functions, including (I believe) Cython-ized Python code.
libfaketime
python-libfaketime
is a much less popular library, but it is much faster.
It wraps the LD_PRELOAD
library libfaketime, which replaces all the C-level system calls for the current time with its own wrappers.
It’s therefore a “perfect” mock, affecting every single point the current time might be fetched from the current process.
It also has much the same API:
import datetime as dt import libfaketime @libfaketime.fake_time("1955-11-05 01:22") def test_delorean(): assert dt.date.today().isoformat() == "1955-11-05"
The approach is much faster since starting the mock only requires changing an environment variable that libfaketime reads. The python-libfaketime README has a benchmark showing it working 300 times faster than freezegun. This benchmark even puts freezegun at an advantage, since it doesn’t import any extra dependencies, and freezegun’s runtime is proportional to the number of imported modules.
I learnt about python-libfaketime at YPlan, where we were first using freezegun. Moving to python-libfaketime took our Django project’s test suite (of several thousand tests) from 5 minutes to 3 minutes.
Unfortunately python-libfaketime comes with the limitations of LD_PRELOAD
.
This is a mechanism to replace system libraries for a program as it loads ( explanation
).
This causes two issues in particular when you use python-libfaketime.
First, LD_PRELOAD
is only available on Unix platforms, which prevents you from using it on Windows.
This can be a blocker for many teams.
Second, you have to help manage LD_PRELOAD
.
You either use python-libfaketime’s reexec_if_needed()
function, which restarts (re-execs) your test process while loading, or manually manage the LD_PRELOAD
environment variable.
Neither is ideal.
Re-execing breaks anything that might wrap your test process, such as profilers, debuggers, and IDE test runners.
Manually managing the environment variable is a bit of overhead, and must be done for each environment you run your tests in, including each developer’s machine.
time-machine
My new library, time-machine
, is intended to combine the advantages of freezegun and libfaketime.
It works without LD_PRELOAD
but still mocks the standard library functions everywhere they may be referenced.
It does so by modifying the built-in functions at the C level, to point them through wrappers that return different values when mocking.
Normally in Python, built-in functions are immutable, but time-machine overcomes this by using C code to replace their function pointers.
Again, it has much the same API as freezegun, except from the names:
import datetime as dt import time_machine @time_machine.travel("1955-11-05 01:22") def test_delorean(): assert dt.date.today().isoformat() == "1955-11-05"
Its weak point is that libraries making their own system calls won’t be mocked.
(Cython use of the datetime
and time
modules should be mocked, although I haven’t tested it yet).
However I believe such usage is rare in Python programs - freezegun also shares this weakness, but that hasn’t stopped it becoming popular.
If you have time, please try out time-machine in your tests! It’s available now for Python 3.6+. Because of its implementation, it only works with CPython, not PyPy or any other interpreters.
It’s my first open source project using a C extension. Let me know how it works, and if you’re switching from freezegun, how much it speeds up your tests. If it is found to work well, it may be possible to merge its technique into freezegun, to share the speed boost without causing churn.
Fin
May time make you ever stronger,
—Adam
Are your Django project's tests slow?Read Speed Up Your Django Tests now!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。