Introducing time-machine, a new Python library for mocking the current time

栏目: IT技术 · 发布时间: 4年前

内容简介: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
Introducing time-machine, a new Python library for mocking the current time

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!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数字乌托邦

数字乌托邦

尼古拉斯•卡尔 / 姜忠伟 / 中信前沿出版社 / 2018-5 / 69.00

当下,技术与我们的关系变得越来越紧密不可分割,特别是智能手机等设备的出现,带给整个人类社会一场彻底的变革。的确,智能手机上的各种应用程序让我们的工作生活无比便利:社交媒体让我们能够和他人实时保持联络并传输信息,不再受时间、地点的限制;搜索引擎通过精准的算法将我们所需要的信息整合推送至屏幕上,让我们毫不费力就看到自己想要的;地图软件为我们的出行提供了更多路线选择,甚至可以使用语音导航,帮助我们顺利到......一起来看看 《数字乌托邦》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具